@rawsql-ts/ztd-cli 0.20.0 → 0.20.2
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/LICENSE +21 -0
- package/dist/commands/agents.d.ts +2 -0
- package/dist/commands/agents.js +68 -0
- package/dist/commands/agents.js.map +1 -0
- package/dist/commands/checkContract.d.ts +46 -0
- package/dist/commands/checkContract.js +359 -0
- package/dist/commands/checkContract.js.map +1 -0
- package/dist/commands/connectionOptions.d.ts +12 -0
- package/dist/commands/connectionOptions.js +22 -0
- package/dist/commands/connectionOptions.js.map +1 -0
- package/dist/commands/ddl.d.ts +7 -0
- package/dist/commands/ddl.js +145 -0
- package/dist/commands/ddl.js.map +1 -0
- package/dist/commands/describe.d.ts +23 -0
- package/dist/commands/describe.js +399 -0
- package/dist/commands/describe.js.map +1 -0
- package/dist/commands/diff.d.ts +24 -0
- package/dist/commands/diff.js +73 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/genEntities.d.ts +14 -0
- package/dist/commands/genEntities.js +58 -0
- package/dist/commands/genEntities.js.map +1 -0
- package/dist/commands/init.d.ts +104 -0
- package/dist/commands/init.js +1480 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/lint.d.ts +89 -0
- package/dist/commands/lint.js +501 -0
- package/dist/commands/lint.js.map +1 -0
- package/dist/commands/modelGen.d.ts +60 -0
- package/dist/commands/modelGen.js +572 -0
- package/dist/commands/modelGen.js.map +1 -0
- package/dist/commands/options.d.ts +9 -0
- package/dist/commands/options.js +48 -0
- package/dist/commands/options.js.map +1 -0
- package/dist/commands/perf.d.ts +9 -0
- package/dist/commands/perf.js +374 -0
- package/dist/commands/perf.js.map +1 -0
- package/dist/commands/pull.d.ts +21 -0
- package/dist/commands/pull.js +115 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/query.d.ts +9 -0
- package/dist/commands/query.js +377 -0
- package/dist/commands/query.js.map +1 -0
- package/dist/commands/testEvidence.d.ts +237 -0
- package/dist/commands/testEvidence.js +1220 -0
- package/dist/commands/testEvidence.js.map +1 -0
- package/dist/commands/ztdConfig.d.ts +30 -0
- package/dist/commands/ztdConfig.js +224 -0
- package/dist/commands/ztdConfig.js.map +1 -0
- package/dist/commands/ztdConfigCommand.d.ts +18 -0
- package/dist/commands/ztdConfigCommand.js +268 -0
- package/dist/commands/ztdConfigCommand.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +127 -0
- package/dist/index.js.map +1 -0
- package/dist/perf/benchmark.d.ts +277 -0
- package/dist/perf/benchmark.js +2186 -0
- package/dist/perf/benchmark.js.map +1 -0
- package/dist/perf/sandbox.d.ts +73 -0
- package/dist/perf/sandbox.js +492 -0
- package/dist/perf/sandbox.js.map +1 -0
- package/dist/query/analysis.d.ts +20 -0
- package/dist/query/analysis.js +192 -0
- package/dist/query/analysis.js.map +1 -0
- package/dist/query/analyzeColumnUsage.d.ts +10 -0
- package/dist/query/analyzeColumnUsage.js +451 -0
- package/dist/query/analyzeColumnUsage.js.map +1 -0
- package/dist/query/analyzeTableUsage.d.ts +10 -0
- package/dist/query/analyzeTableUsage.js +318 -0
- package/dist/query/analyzeTableUsage.js.map +1 -0
- package/dist/query/execute.d.ts +40 -0
- package/dist/query/execute.js +784 -0
- package/dist/query/execute.js.map +1 -0
- package/dist/query/format.d.ts +1 -0
- package/dist/query/format.js +9 -0
- package/dist/query/format.js.map +1 -0
- package/dist/query/lint.d.ts +29 -0
- package/dist/query/lint.js +340 -0
- package/dist/query/lint.js.map +1 -0
- package/dist/query/location.d.ts +18 -0
- package/dist/query/location.js +204 -0
- package/dist/query/location.js.map +1 -0
- package/dist/query/patch.d.ts +21 -0
- package/dist/query/patch.js +151 -0
- package/dist/query/patch.js.map +1 -0
- package/dist/query/planner.d.ts +31 -0
- package/dist/query/planner.js +134 -0
- package/dist/query/planner.js.map +1 -0
- package/dist/query/report.d.ts +7 -0
- package/dist/query/report.js +19 -0
- package/dist/query/report.js.map +1 -0
- package/dist/query/scalarFilterAnalysis.d.ts +6 -0
- package/dist/query/scalarFilterAnalysis.js +212 -0
- package/dist/query/scalarFilterAnalysis.js.map +1 -0
- package/dist/query/slice.d.ts +17 -0
- package/dist/query/slice.js +204 -0
- package/dist/query/slice.js.map +1 -0
- package/dist/query/structure.d.ts +24 -0
- package/dist/query/structure.js +135 -0
- package/dist/query/structure.js.map +1 -0
- package/dist/query/targets.d.ts +2 -0
- package/dist/query/targets.js +6 -0
- package/dist/query/targets.js.map +1 -0
- package/dist/query/types.d.ts +97 -0
- package/dist/query/types.js +3 -0
- package/dist/query/types.js.map +1 -0
- package/dist/specs/sql/activeOrders.catalog.d.ts +12 -0
- package/dist/specs/sql/activeOrders.catalog.js +36 -0
- package/dist/specs/sql/activeOrders.catalog.js.map +1 -0
- package/dist/specs/sql/usersList.catalog.d.ts +8 -0
- package/dist/specs/sql/usersList.catalog.js +14 -0
- package/dist/specs/sql/usersList.catalog.js.map +1 -0
- package/dist/specs/sqlCatalogDefinition.d.ts +20 -0
- package/dist/specs/sqlCatalogDefinition.js +10 -0
- package/dist/specs/sqlCatalogDefinition.js.map +1 -0
- package/dist/utils/agentCli.d.ts +23 -0
- package/dist/utils/agentCli.js +84 -0
- package/dist/utils/agentCli.js.map +1 -0
- package/dist/utils/agentSafety.d.ts +4 -0
- package/dist/utils/agentSafety.js +50 -0
- package/dist/utils/agentSafety.js.map +1 -0
- package/dist/utils/agents.d.ts +31 -0
- package/dist/utils/agents.js +362 -0
- package/dist/utils/agents.js.map +1 -0
- package/dist/utils/collectSqlFiles.d.ts +9 -0
- package/dist/utils/collectSqlFiles.js +58 -0
- package/dist/utils/collectSqlFiles.js.map +1 -0
- package/dist/utils/connectionSummary.d.ts +3 -0
- package/dist/utils/connectionSummary.js +29 -0
- package/dist/utils/connectionSummary.js.map +1 -0
- package/dist/utils/dbConnection.d.ts +31 -0
- package/dist/utils/dbConnection.js +151 -0
- package/dist/utils/dbConnection.js.map +1 -0
- package/dist/utils/fs.d.ts +1 -0
- package/dist/utils/fs.js +12 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/modelGenBinder.d.ts +8 -0
- package/dist/utils/modelGenBinder.js +31 -0
- package/dist/utils/modelGenBinder.js.map +1 -0
- package/dist/utils/modelGenRender.d.ts +29 -0
- package/dist/utils/modelGenRender.js +158 -0
- package/dist/utils/modelGenRender.js.map +1 -0
- package/dist/utils/modelGenScanner.d.ts +24 -0
- package/dist/utils/modelGenScanner.js +196 -0
- package/dist/utils/modelGenScanner.js.map +1 -0
- package/dist/utils/modelProbe.d.ts +14 -0
- package/dist/utils/modelProbe.js +121 -0
- package/dist/utils/modelProbe.js.map +1 -0
- package/dist/utils/normalizePulledSchema.d.ts +12 -0
- package/dist/utils/normalizePulledSchema.js +213 -0
- package/dist/utils/normalizePulledSchema.js.map +1 -0
- package/dist/utils/optionalDependencies.d.ts +43 -0
- package/dist/utils/optionalDependencies.js +134 -0
- package/dist/utils/optionalDependencies.js.map +1 -0
- package/dist/utils/pgDump.d.ts +12 -0
- package/dist/utils/pgDump.js +58 -0
- package/dist/utils/pgDump.js.map +1 -0
- package/dist/utils/queryFingerprint.d.ts +14 -0
- package/dist/utils/queryFingerprint.js +34 -0
- package/dist/utils/queryFingerprint.js.map +1 -0
- package/dist/utils/sqlCatalogDiscovery.d.ts +44 -0
- package/dist/utils/sqlCatalogDiscovery.js +166 -0
- package/dist/utils/sqlCatalogDiscovery.js.map +1 -0
- package/dist/utils/sqlCatalogStatements.d.ts +20 -0
- package/dist/utils/sqlCatalogStatements.js +23 -0
- package/dist/utils/sqlCatalogStatements.js.map +1 -0
- package/dist/utils/sqlLintHelpers.d.ts +18 -0
- package/dist/utils/sqlLintHelpers.js +270 -0
- package/dist/utils/sqlLintHelpers.js.map +1 -0
- package/dist/utils/telemetry.d.ts +71 -0
- package/dist/utils/telemetry.js +597 -0
- package/dist/utils/telemetry.js.map +1 -0
- package/dist/utils/typeMapper.d.ts +4 -0
- package/dist/utils/typeMapper.js +79 -0
- package/dist/utils/typeMapper.js.map +1 -0
- package/dist/utils/ztdProjectConfig.d.ts +41 -0
- package/dist/utils/ztdProjectConfig.js +182 -0
- package/dist/utils/ztdProjectConfig.js.map +1 -0
- package/package.json +19 -20
|
@@ -0,0 +1,1220 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TestEvidenceRuntimeError = void 0;
|
|
7
|
+
exports.resolveTestEvidenceExitCode = resolveTestEvidenceExitCode;
|
|
8
|
+
exports.registerTestEvidenceCommand = registerTestEvidenceCommand;
|
|
9
|
+
exports.runTestEvidenceSpecification = runTestEvidenceSpecification;
|
|
10
|
+
exports.formatTestEvidenceOutput = formatTestEvidenceOutput;
|
|
11
|
+
exports.stableStringify = stableStringify;
|
|
12
|
+
exports.buildTestEvidencePrDiff = buildTestEvidencePrDiff;
|
|
13
|
+
exports.formatTestEvidencePrMarkdown = formatTestEvidencePrMarkdown;
|
|
14
|
+
exports.runTestEvidencePr = runTestEvidencePr;
|
|
15
|
+
exports.applyEvidenceOutputControls = applyEvidenceOutputControls;
|
|
16
|
+
exports.formatTestDocumentationOutput = formatTestDocumentationOutput;
|
|
17
|
+
const node_fs_1 = require("node:fs");
|
|
18
|
+
const node_module_1 = require("node:module");
|
|
19
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
20
|
+
const node_child_process_1 = require("node:child_process");
|
|
21
|
+
const test_evidence_core_1 = require("@rawsql-ts/test-evidence-core");
|
|
22
|
+
const test_evidence_renderer_md_1 = require("@rawsql-ts/test-evidence-renderer-md");
|
|
23
|
+
const sqlCatalogDiscovery_1 = require("../utils/sqlCatalogDiscovery");
|
|
24
|
+
const agentCli_1 = require("../utils/agentCli");
|
|
25
|
+
/**
|
|
26
|
+
* Runtime/configuration error for test evidence command (maps to exit code 2).
|
|
27
|
+
*/
|
|
28
|
+
class TestEvidenceRuntimeError extends Error {
|
|
29
|
+
constructor(message, options) {
|
|
30
|
+
super(message);
|
|
31
|
+
this.exitCode = 2;
|
|
32
|
+
this.name = 'TestEvidenceRuntimeError';
|
|
33
|
+
this.code = options === null || options === void 0 ? void 0 : options.code;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
exports.TestEvidenceRuntimeError = TestEvidenceRuntimeError;
|
|
37
|
+
/**
|
|
38
|
+
* Resolve command exit code for test evidence generation.
|
|
39
|
+
* @param args.result Completed generation result when execution succeeded.
|
|
40
|
+
* @param args.error Error thrown while generating evidence.
|
|
41
|
+
* @returns 0 when generation succeeded, 2 for runtime/configuration errors, 1 for other failures.
|
|
42
|
+
*/
|
|
43
|
+
function resolveTestEvidenceExitCode(args) {
|
|
44
|
+
if (args.error) {
|
|
45
|
+
return args.error instanceof TestEvidenceRuntimeError ? 2 : 1;
|
|
46
|
+
}
|
|
47
|
+
if (!args.result) {
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Register `ztd evidence` command on the CLI root program.
|
|
54
|
+
*/
|
|
55
|
+
function registerTestEvidenceCommand(program) {
|
|
56
|
+
const evidenceCommand = program
|
|
57
|
+
.command('evidence')
|
|
58
|
+
.alias('test-evidence')
|
|
59
|
+
.description('Generate deterministic test specification evidence artifacts')
|
|
60
|
+
.option('--mode <mode>', 'Evidence mode (specification)', 'specification')
|
|
61
|
+
.option('--format <format>', 'Output format (json|markdown|both)', 'both')
|
|
62
|
+
.option('--out-dir <path>', 'Output directory', '.ztd/test-evidence')
|
|
63
|
+
.option('--specs-dir <path>', 'Override SQL catalog specs directory (default: src/catalog/specs)')
|
|
64
|
+
.option('--tests-dir <path>', 'Override tests directory (default: tests)')
|
|
65
|
+
.option('--spec-module <path>', 'Explicit evidence module path (default: tests/specs/index)')
|
|
66
|
+
.option('--json <payload>', 'Pass evidence options as a JSON object')
|
|
67
|
+
.option('--summary-only', 'Write only summary counts without catalog or case detail payloads')
|
|
68
|
+
.option('--limit <count>', 'Limit returned catalogs and cases in generated artifacts')
|
|
69
|
+
.action((options) => {
|
|
70
|
+
var _a;
|
|
71
|
+
try {
|
|
72
|
+
const merged = options.json ? { ...options, ...(0, agentCli_1.parseJsonPayload)(options.json, '--json') } : options;
|
|
73
|
+
const mode = normalizeMode(String(merged.mode));
|
|
74
|
+
const format = normalizeFormat(String(merged.format));
|
|
75
|
+
const report = applyEvidenceOutputControls(runTestEvidenceSpecification({
|
|
76
|
+
mode,
|
|
77
|
+
rootDir: process.env.ZTD_PROJECT_ROOT,
|
|
78
|
+
specsDir: merged.specsDir,
|
|
79
|
+
testsDir: merged.testsDir,
|
|
80
|
+
specModule: merged.specModule
|
|
81
|
+
}), {
|
|
82
|
+
summaryOnly: Boolean(merged.summaryOnly),
|
|
83
|
+
limit: normalizeEvidenceLimit(merged.limit)
|
|
84
|
+
});
|
|
85
|
+
const sourceRootDir = node_path_1.default.resolve((_a = process.env.ZTD_PROJECT_ROOT) !== null && _a !== void 0 ? _a : process.cwd());
|
|
86
|
+
writeArtifacts({
|
|
87
|
+
report,
|
|
88
|
+
format,
|
|
89
|
+
outDir: node_path_1.default.resolve(process.cwd(), String(merged.outDir)),
|
|
90
|
+
sourceRootDir
|
|
91
|
+
});
|
|
92
|
+
process.exitCode = resolveTestEvidenceExitCode({ result: report });
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
process.exitCode = resolveTestEvidenceExitCode({ error });
|
|
96
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
evidenceCommand
|
|
100
|
+
.command('test-doc')
|
|
101
|
+
.description('Generate human-readable Markdown test documentation from ZTD test assets')
|
|
102
|
+
.option('--out <path>', 'Output markdown path', '.ztd/test-evidence/test-documentation.md')
|
|
103
|
+
.option('--specs-dir <path>', 'Override SQL catalog specs directory (default: src/catalog/specs)')
|
|
104
|
+
.option('--tests-dir <path>', 'Override tests directory (default: tests)')
|
|
105
|
+
.option('--spec-module <path>', 'Explicit evidence module path (default: tests/specs/index)')
|
|
106
|
+
.option('--json <payload>', 'Pass test-doc options as a JSON object')
|
|
107
|
+
.action((options) => {
|
|
108
|
+
var _a, _b;
|
|
109
|
+
try {
|
|
110
|
+
const merged = options.json ? { ...options, ...(0, agentCli_1.parseJsonPayload)(options.json, '--json') } : options;
|
|
111
|
+
const report = runTestEvidenceSpecification({
|
|
112
|
+
mode: 'specification',
|
|
113
|
+
rootDir: process.env.ZTD_PROJECT_ROOT,
|
|
114
|
+
specsDir: merged.specsDir,
|
|
115
|
+
testsDir: merged.testsDir,
|
|
116
|
+
specModule: merged.specModule
|
|
117
|
+
});
|
|
118
|
+
const sourceRootDir = node_path_1.default.resolve((_a = process.env.ZTD_PROJECT_ROOT) !== null && _a !== void 0 ? _a : process.cwd());
|
|
119
|
+
writeTestDocumentationArtifact({
|
|
120
|
+
report,
|
|
121
|
+
outPath: node_path_1.default.resolve(process.cwd(), String((_b = merged.out) !== null && _b !== void 0 ? _b : '.ztd/test-evidence/test-documentation.md')),
|
|
122
|
+
sourceRootDir
|
|
123
|
+
});
|
|
124
|
+
process.exitCode = resolveTestEvidenceExitCode({ result: report });
|
|
125
|
+
}
|
|
126
|
+
catch (error) {
|
|
127
|
+
process.exitCode = resolveTestEvidenceExitCode({ error });
|
|
128
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
evidenceCommand
|
|
132
|
+
.command('pr')
|
|
133
|
+
.description('Generate PR diff evidence from base/head specification JSON projections')
|
|
134
|
+
.option('--base <ref>', 'Base git ref', 'main')
|
|
135
|
+
.option('--head <ref>', 'Head git ref', 'HEAD')
|
|
136
|
+
.option('--base-mode <mode>', 'Base resolution mode (merge-base|ref)', 'merge-base')
|
|
137
|
+
.option('--allow-empty-base', 'Allow empty base evidence when head has catalogs')
|
|
138
|
+
.option('--removed-detail <level>', 'Removed case detail level (none|input|full)', 'input')
|
|
139
|
+
.option('--out-dir <path>', 'Output directory', 'artifacts/test-evidence')
|
|
140
|
+
.option('--specs-dir <path>', 'Override SQL catalog specs directory (default: src/catalog/specs)')
|
|
141
|
+
.option('--tests-dir <path>', 'Override tests directory (default: tests)')
|
|
142
|
+
.option('--spec-module <path>', 'Explicit evidence module path (default: tests/specs/index)')
|
|
143
|
+
.option('--json <payload>', 'Pass PR evidence options as a JSON object')
|
|
144
|
+
.option('--summary-only', 'Write PR evidence with summary-only base/head projections')
|
|
145
|
+
.option('--limit <count>', 'Limit returned catalogs and cases in base/head projections')
|
|
146
|
+
.action((options) => {
|
|
147
|
+
var _a, _b, _c, _d, _e;
|
|
148
|
+
try {
|
|
149
|
+
const merged = options.json ? { ...options, ...(0, agentCli_1.parseJsonPayload)(options.json, '--json') } : options;
|
|
150
|
+
const output = runTestEvidencePr({
|
|
151
|
+
baseRef: String((_a = merged.base) !== null && _a !== void 0 ? _a : 'main'),
|
|
152
|
+
headRef: String((_b = merged.head) !== null && _b !== void 0 ? _b : 'HEAD'),
|
|
153
|
+
baseMode: normalizeBaseMode(String((_c = merged.baseMode) !== null && _c !== void 0 ? _c : 'merge-base')),
|
|
154
|
+
allowEmptyBase: Boolean(merged.allowEmptyBase),
|
|
155
|
+
removedDetail: normalizeRemovedDetail(String((_d = merged.removedDetail) !== null && _d !== void 0 ? _d : 'input')),
|
|
156
|
+
outDir: String((_e = merged.outDir) !== null && _e !== void 0 ? _e : 'artifacts/test-evidence'),
|
|
157
|
+
rootDir: process.env.ZTD_PROJECT_ROOT,
|
|
158
|
+
specsDir: merged.specsDir,
|
|
159
|
+
testsDir: merged.testsDir,
|
|
160
|
+
specModule: merged.specModule,
|
|
161
|
+
summaryOnly: Boolean(merged.summaryOnly),
|
|
162
|
+
limit: normalizeEvidenceLimit(merged.limit)
|
|
163
|
+
});
|
|
164
|
+
process.exitCode = resolveTestEvidenceExitCode({ result: output.headReport });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
process.exitCode = resolveTestEvidenceExitCode({ error });
|
|
168
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function normalizeMode(mode) {
|
|
173
|
+
const normalized = mode.trim().toLowerCase();
|
|
174
|
+
if (normalized !== 'specification') {
|
|
175
|
+
throw new TestEvidenceRuntimeError(`Unsupported mode: ${mode}`);
|
|
176
|
+
}
|
|
177
|
+
return 'specification';
|
|
178
|
+
}
|
|
179
|
+
function normalizeFormat(format) {
|
|
180
|
+
const normalized = format.trim().toLowerCase();
|
|
181
|
+
if (normalized === 'json' || normalized === 'markdown' || normalized === 'both') {
|
|
182
|
+
return normalized;
|
|
183
|
+
}
|
|
184
|
+
throw new TestEvidenceRuntimeError(`Unsupported format: ${format}`);
|
|
185
|
+
}
|
|
186
|
+
function normalizeBaseMode(mode) {
|
|
187
|
+
const normalized = mode.trim().toLowerCase();
|
|
188
|
+
if (normalized === 'merge-base' || normalized === 'ref') {
|
|
189
|
+
return normalized;
|
|
190
|
+
}
|
|
191
|
+
throw new TestEvidenceRuntimeError(`Unsupported base-mode: ${mode}`);
|
|
192
|
+
}
|
|
193
|
+
function normalizeRemovedDetail(level) {
|
|
194
|
+
const normalized = level.trim().toLowerCase();
|
|
195
|
+
if (normalized === 'none' || normalized === 'input' || normalized === 'full') {
|
|
196
|
+
return normalized;
|
|
197
|
+
}
|
|
198
|
+
throw new TestEvidenceRuntimeError(`Unsupported removed-detail: ${level}`);
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Build deterministic specification evidence from SQL catalog specs and test-case catalog exports.
|
|
202
|
+
*/
|
|
203
|
+
function runTestEvidenceSpecification(options) {
|
|
204
|
+
var _a;
|
|
205
|
+
const root = node_path_1.default.resolve((_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd());
|
|
206
|
+
const specsDir = options.specsDir ? node_path_1.default.resolve(root, options.specsDir) : node_path_1.default.resolve(root, 'src', 'catalog', 'specs');
|
|
207
|
+
const testsDir = options.testsDir ? node_path_1.default.resolve(root, options.testsDir) : node_path_1.default.resolve(root, 'tests');
|
|
208
|
+
const sqlSpecFiles = (0, node_fs_1.existsSync)(specsDir) ? (0, sqlCatalogDiscovery_1.walkSqlCatalogSpecFiles)(specsDir) : [];
|
|
209
|
+
const evidenceModule = loadEvidenceModule(root, testsDir, options.specModule);
|
|
210
|
+
const testCaseCatalogFiles = (0, node_fs_1.existsSync)(testsDir) ? walkFiles(testsDir, isTestCaseCatalogFile) : [];
|
|
211
|
+
const legacyTestCases = evidenceModule ? [] : testCaseCatalogFiles.flatMap((filePath) => loadTestCaseCatalogEvidence(root, filePath));
|
|
212
|
+
const testCaseCatalogs = evidenceModule
|
|
213
|
+
? readTestCaseCatalogsFromModule(evidenceModule)
|
|
214
|
+
.map((catalog) => ({
|
|
215
|
+
...catalog,
|
|
216
|
+
cases: [...catalog.cases].sort((a, b) => a.id.localeCompare(b.id))
|
|
217
|
+
}))
|
|
218
|
+
.sort((a, b) => a.id.localeCompare(b.id))
|
|
219
|
+
: [];
|
|
220
|
+
const testCasesFromModule = flattenTestCaseCatalogs(testCaseCatalogs);
|
|
221
|
+
const sqlCaseCatalogsFromModule = evidenceModule ? readSqlCaseCatalogsFromModule(evidenceModule) : [];
|
|
222
|
+
if (sqlSpecFiles.length === 0 && testCasesFromModule.length === 0 && legacyTestCases.length === 0 && sqlCaseCatalogsFromModule.length === 0) {
|
|
223
|
+
throw new TestEvidenceRuntimeError(`No catalog specs or test-case evidence exports were found. Checked specsDir=${specsDir}, testsDir=${testsDir}`, { code: 'NO_SPECS_FOUND' });
|
|
224
|
+
}
|
|
225
|
+
const sqlCatalogs = sqlSpecFiles
|
|
226
|
+
.flatMap((filePath) => (0, sqlCatalogDiscovery_1.loadSqlCatalogSpecsFromFile)(filePath, (message) => new TestEvidenceRuntimeError(message)))
|
|
227
|
+
.map((loaded) => toSqlEvidence(root, loaded))
|
|
228
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.specFile.localeCompare(b.specFile));
|
|
229
|
+
const testCases = [...testCasesFromModule, ...legacyTestCases]
|
|
230
|
+
.sort((a, b) => a.id.localeCompare(b.id) || a.filePath.localeCompare(b.filePath));
|
|
231
|
+
const sqlCaseCatalogs = sqlCaseCatalogsFromModule
|
|
232
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
233
|
+
return {
|
|
234
|
+
schemaVersion: 1,
|
|
235
|
+
mode: options.mode,
|
|
236
|
+
summary: {
|
|
237
|
+
sqlCatalogCount: sqlCatalogs.length,
|
|
238
|
+
sqlCaseCatalogCount: sqlCaseCatalogs.length,
|
|
239
|
+
testCaseCount: testCases.length,
|
|
240
|
+
specFilesScanned: sqlSpecFiles.length,
|
|
241
|
+
testFilesScanned: evidenceModule ? 1 : testCaseCatalogFiles.length
|
|
242
|
+
},
|
|
243
|
+
sqlCatalogs,
|
|
244
|
+
sqlCaseCatalogs,
|
|
245
|
+
testCaseCatalogs,
|
|
246
|
+
testCases
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Render deterministic JSON or Markdown output text.
|
|
251
|
+
*/
|
|
252
|
+
function formatTestEvidenceOutput(report, format, context) {
|
|
253
|
+
var _a;
|
|
254
|
+
if (format === 'json') {
|
|
255
|
+
return `${JSON.stringify(report, null, 2)}\n`;
|
|
256
|
+
}
|
|
257
|
+
if ((_a = report.display) === null || _a === void 0 ? void 0 : _a.summaryOnly) {
|
|
258
|
+
const lines = [
|
|
259
|
+
'# Test Specification Summary',
|
|
260
|
+
'',
|
|
261
|
+
`- schemaVersion: ${report.schemaVersion}`,
|
|
262
|
+
`- mode: ${report.mode}`,
|
|
263
|
+
`- sqlCatalogCount: ${report.summary.sqlCatalogCount}`,
|
|
264
|
+
`- sqlCaseCatalogCount: ${report.summary.sqlCaseCatalogCount}`,
|
|
265
|
+
`- testCaseCount: ${report.summary.testCaseCount}`,
|
|
266
|
+
`- specFilesScanned: ${report.summary.specFilesScanned}`,
|
|
267
|
+
`- testFilesScanned: ${report.summary.testFilesScanned}`,
|
|
268
|
+
`- truncated: ${report.display.truncated}`,
|
|
269
|
+
report.display.limit !== undefined ? `- limit: ${report.display.limit}` : ''
|
|
270
|
+
].filter(Boolean);
|
|
271
|
+
return `${lines.join('\n')}\n`;
|
|
272
|
+
}
|
|
273
|
+
const model = (0, test_evidence_core_1.buildSpecificationModel)(report);
|
|
274
|
+
return `${(0, test_evidence_renderer_md_1.renderSpecificationMarkdown)(model, {
|
|
275
|
+
definitionLinks: resolveDefinitionLinkOptions(context)
|
|
276
|
+
})}\n`;
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Stable stringify that sorts object keys recursively for deterministic fingerprinting.
|
|
280
|
+
*/
|
|
281
|
+
function stableStringify(value) {
|
|
282
|
+
return (0, test_evidence_core_1.stableStringify)(value);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Build deterministic PR diff JSON from base/head specification reports.
|
|
286
|
+
*/
|
|
287
|
+
function buildTestEvidencePrDiff(args) {
|
|
288
|
+
return (0, test_evidence_core_1.buildDiffJson)({
|
|
289
|
+
base: {
|
|
290
|
+
ref: args.base.ref,
|
|
291
|
+
sha: args.base.sha,
|
|
292
|
+
previewJson: args.base.report
|
|
293
|
+
},
|
|
294
|
+
head: {
|
|
295
|
+
ref: args.head.ref,
|
|
296
|
+
sha: args.head.sha,
|
|
297
|
+
previewJson: args.head.report
|
|
298
|
+
},
|
|
299
|
+
baseMode: args.baseMode
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Render PR-focused markdown from diff JSON.
|
|
304
|
+
*/
|
|
305
|
+
function formatTestEvidencePrMarkdown(diff, options) {
|
|
306
|
+
return (0, test_evidence_renderer_md_1.renderDiffMarkdown)(diff, {
|
|
307
|
+
definitionLinks: resolveDefinitionLinkOptions({
|
|
308
|
+
markdownPath: options === null || options === void 0 ? void 0 : options.markdownPath,
|
|
309
|
+
sourceRootDir: options === null || options === void 0 ? void 0 : options.sourceRootDir
|
|
310
|
+
})
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
function resolveDefinitionLinkOptions(context) {
|
|
314
|
+
var _a, _b, _c;
|
|
315
|
+
const serverUrl = (_a = process.env.GITHUB_SERVER_URL) === null || _a === void 0 ? void 0 : _a.trim();
|
|
316
|
+
const repository = (_b = process.env.GITHUB_REPOSITORY) === null || _b === void 0 ? void 0 : _b.trim();
|
|
317
|
+
const ref = (_c = process.env.GITHUB_SHA) === null || _c === void 0 ? void 0 : _c.trim();
|
|
318
|
+
if (serverUrl && repository && ref) {
|
|
319
|
+
return {
|
|
320
|
+
mode: 'github',
|
|
321
|
+
github: {
|
|
322
|
+
serverUrl,
|
|
323
|
+
repository,
|
|
324
|
+
ref
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
if ((context === null || context === void 0 ? void 0 : context.markdownPath) && (context === null || context === void 0 ? void 0 : context.sourceRootDir)) {
|
|
329
|
+
return {
|
|
330
|
+
mode: 'path',
|
|
331
|
+
path: {
|
|
332
|
+
markdownDir: node_path_1.default.dirname(context.markdownPath),
|
|
333
|
+
sourceRootDir: context.sourceRootDir
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
return { mode: 'path' };
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Generate base/head reports, compute PR diff JSON, and write PR artifacts.
|
|
341
|
+
*/
|
|
342
|
+
function runTestEvidencePr(options) {
|
|
343
|
+
var _a;
|
|
344
|
+
const root = node_path_1.default.resolve((_a = options.rootDir) !== null && _a !== void 0 ? _a : process.cwd());
|
|
345
|
+
const tempRoot = node_path_1.default.resolve(root, '.tmp', 'test-evidence-worktree');
|
|
346
|
+
(0, node_fs_1.mkdirSync)(tempRoot, { recursive: true });
|
|
347
|
+
const createdWorktrees = [];
|
|
348
|
+
const resolvedHeadSha = resolveGitSha(root, options.headRef);
|
|
349
|
+
const resolvedBaseSha = options.baseMode === 'merge-base'
|
|
350
|
+
? resolveGitMergeBase(root, options.baseRef, options.headRef)
|
|
351
|
+
: resolveGitSha(root, options.baseRef);
|
|
352
|
+
const currentHeadSha = resolveGitSha(root, 'HEAD');
|
|
353
|
+
try {
|
|
354
|
+
const headMaterialized = materializeEvidenceForRef({
|
|
355
|
+
repoRoot: root,
|
|
356
|
+
ref: options.headRef,
|
|
357
|
+
resolvedSha: resolvedHeadSha,
|
|
358
|
+
allowCurrentWorkspace: resolvedHeadSha === currentHeadSha,
|
|
359
|
+
tempRoot,
|
|
360
|
+
createdWorktrees,
|
|
361
|
+
specsDir: options.specsDir,
|
|
362
|
+
testsDir: options.testsDir,
|
|
363
|
+
specModule: options.specModule,
|
|
364
|
+
summaryOnly: options.summaryOnly,
|
|
365
|
+
limit: options.limit
|
|
366
|
+
});
|
|
367
|
+
const baseMaterialized = materializeEvidenceForRef({
|
|
368
|
+
repoRoot: root,
|
|
369
|
+
ref: options.baseRef,
|
|
370
|
+
resolvedSha: resolvedBaseSha,
|
|
371
|
+
allowCurrentWorkspace: resolvedBaseSha === currentHeadSha,
|
|
372
|
+
tempRoot,
|
|
373
|
+
createdWorktrees,
|
|
374
|
+
specsDir: options.specsDir,
|
|
375
|
+
testsDir: options.testsDir,
|
|
376
|
+
specModule: options.specModule,
|
|
377
|
+
summaryOnly: options.summaryOnly,
|
|
378
|
+
limit: options.limit
|
|
379
|
+
});
|
|
380
|
+
console.log(`base preview: ${baseMaterialized.previewJsonPath}`);
|
|
381
|
+
console.log(`head preview: ${headMaterialized.previewJsonPath}`);
|
|
382
|
+
const diff = buildTestEvidencePrDiff({
|
|
383
|
+
base: { ref: options.baseRef, sha: baseMaterialized.sha, report: baseMaterialized.report },
|
|
384
|
+
head: { ref: options.headRef, sha: headMaterialized.sha, report: headMaterialized.report },
|
|
385
|
+
baseMode: options.baseMode
|
|
386
|
+
});
|
|
387
|
+
if (!options.allowEmptyBase && diff.totals.base.catalogs === 0 && diff.totals.head.catalogs > 0) {
|
|
388
|
+
throw new TestEvidenceRuntimeError('Base test evidence is empty.\nThis likely indicates preview generation failed at base ref.\nIf this is intentional, re-run with --allow-empty-base.');
|
|
389
|
+
}
|
|
390
|
+
const outDir = node_path_1.default.resolve(root, options.outDir);
|
|
391
|
+
(0, node_fs_1.mkdirSync)(outDir, { recursive: true });
|
|
392
|
+
const diffJsonPath = node_path_1.default.join(outDir, 'test-specification.pr.json');
|
|
393
|
+
const diffMdPath = node_path_1.default.join(outDir, 'test-specification.pr.md');
|
|
394
|
+
(0, node_fs_1.writeFileSync)(diffJsonPath, `${JSON.stringify(diff, null, 2)}\n`, 'utf8');
|
|
395
|
+
(0, node_fs_1.writeFileSync)(diffMdPath, formatTestEvidencePrMarkdown(diff, {
|
|
396
|
+
removedDetail: options.removedDetail,
|
|
397
|
+
markdownPath: diffMdPath,
|
|
398
|
+
sourceRootDir: root
|
|
399
|
+
}), 'utf8');
|
|
400
|
+
console.log(`wrote: ${diffJsonPath}`);
|
|
401
|
+
console.log(`wrote: ${diffMdPath}`);
|
|
402
|
+
return {
|
|
403
|
+
baseReport: baseMaterialized.report,
|
|
404
|
+
headReport: headMaterialized.report,
|
|
405
|
+
diff
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
finally {
|
|
409
|
+
cleanupWorktrees(root, createdWorktrees);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
function writeArtifacts(args) {
|
|
413
|
+
var _a;
|
|
414
|
+
(0, node_fs_1.mkdirSync)(args.outDir, { recursive: true });
|
|
415
|
+
const writtenFiles = [];
|
|
416
|
+
if (args.format === 'json' || args.format === 'both') {
|
|
417
|
+
const jsonPath = node_path_1.default.join(args.outDir, 'test-specification.json');
|
|
418
|
+
(0, node_fs_1.writeFileSync)(jsonPath, formatTestEvidenceOutput(args.report, 'json'), 'utf8');
|
|
419
|
+
writtenFiles.push(jsonPath);
|
|
420
|
+
}
|
|
421
|
+
if (args.format === 'markdown' || args.format === 'both') {
|
|
422
|
+
const markdownPaths = ((_a = args.report.display) === null || _a === void 0 ? void 0 : _a.summaryOnly)
|
|
423
|
+
? writeSpecificationSummaryMarkdown(args.report, args.outDir)
|
|
424
|
+
: writeSpecificationMarkdownArtifacts(args.report, args.outDir, args.sourceRootDir);
|
|
425
|
+
writtenFiles.push(...markdownPaths);
|
|
426
|
+
}
|
|
427
|
+
for (const filePath of writtenFiles.sort((a, b) => a.localeCompare(b))) {
|
|
428
|
+
console.log(`wrote: ${filePath}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
function writeSpecificationSummaryMarkdown(report, outDir) {
|
|
432
|
+
const summaryPath = node_path_1.default.join(outDir, 'test-specification.summary.md');
|
|
433
|
+
(0, node_fs_1.writeFileSync)(summaryPath, formatTestEvidenceOutput(report, 'markdown'), 'utf8');
|
|
434
|
+
return [summaryPath];
|
|
435
|
+
}
|
|
436
|
+
function writeSpecificationMarkdownArtifacts(report, outDir, sourceRootDir) {
|
|
437
|
+
var _a, _b;
|
|
438
|
+
const indexFileName = 'test-specification.index.md';
|
|
439
|
+
const model = (0, test_evidence_core_1.buildSpecificationModel)(report);
|
|
440
|
+
const catalogs = [...model.catalogs].sort((a, b) => a.catalogId.localeCompare(b.catalogId));
|
|
441
|
+
const written = [];
|
|
442
|
+
const catalogRows = [];
|
|
443
|
+
for (const catalog of catalogs) {
|
|
444
|
+
const catalogSlug = toSpecificationSlug(catalog.catalogId);
|
|
445
|
+
const catalogFileName = `test-specification.catalog.${catalogSlug}.md`;
|
|
446
|
+
const catalogPath = node_path_1.default.join(outDir, catalogFileName);
|
|
447
|
+
const catalogDefinitionLinks = resolveDefinitionLinkOptions({ markdownPath: catalogPath, sourceRootDir });
|
|
448
|
+
const catalogLines = [];
|
|
449
|
+
catalogLines.push(`# ${catalog.catalogId} Test Cases`);
|
|
450
|
+
catalogLines.push('');
|
|
451
|
+
catalogLines.push(`- schemaVersion: ${model.schemaVersion}`);
|
|
452
|
+
catalogLines.push(`- index: [Unit Test Index](./${indexFileName})`);
|
|
453
|
+
catalogLines.push(`- title: ${catalog.title}`);
|
|
454
|
+
const definitionPath = (_a = catalog.definition) !== null && _a !== void 0 ? _a : findCatalogDefinitionPath(report, catalog.catalogId);
|
|
455
|
+
catalogLines.push(`- definition: ${formatDefinitionLinkMarkdown(definitionPath, catalogDefinitionLinks)}`);
|
|
456
|
+
if (catalog.description) {
|
|
457
|
+
catalogLines.push(`- description: ${catalog.description}`);
|
|
458
|
+
}
|
|
459
|
+
if (Array.isArray(catalog.refs) && catalog.refs.length > 0) {
|
|
460
|
+
catalogLines.push('- refs:');
|
|
461
|
+
for (const ref of catalog.refs) {
|
|
462
|
+
catalogLines.push(` - [${ref.label}](${ref.url})`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
catalogLines.push(`- tests: ${catalog.cases.length}`);
|
|
466
|
+
if (catalog.kind === 'sql') {
|
|
467
|
+
catalogLines.push(`- fixtures: ${((_b = catalog.fixtures) !== null && _b !== void 0 ? _b : []).join(', ') || '(none)'}`);
|
|
468
|
+
}
|
|
469
|
+
catalogLines.push('');
|
|
470
|
+
const sortedCases = [...catalog.cases].sort((a, b) => a.id.localeCompare(b.id));
|
|
471
|
+
for (const testCase of sortedCases) {
|
|
472
|
+
catalogLines.push(`## ${testCase.id} - ${testCase.title}`);
|
|
473
|
+
catalogLines.push(`- expected: ${testCase.expected}`);
|
|
474
|
+
catalogLines.push(`- tags: ${formatCaseTags(testCase.tags)}`);
|
|
475
|
+
catalogLines.push(`- focus: ${formatCaseFocus(testCase.focus)}`);
|
|
476
|
+
if (Array.isArray(testCase.refs) && testCase.refs.length > 0) {
|
|
477
|
+
catalogLines.push('- refs:');
|
|
478
|
+
for (const ref of testCase.refs) {
|
|
479
|
+
catalogLines.push(` - [${ref.label}](${ref.url})`);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
catalogLines.push('### input');
|
|
483
|
+
catalogLines.push('```json');
|
|
484
|
+
catalogLines.push(stringifyStablePretty(testCase.input));
|
|
485
|
+
catalogLines.push('```');
|
|
486
|
+
if (testCase.expected === 'throws') {
|
|
487
|
+
catalogLines.push('### error');
|
|
488
|
+
catalogLines.push('```json');
|
|
489
|
+
catalogLines.push(stringifyErrorPretty(testCase.error));
|
|
490
|
+
catalogLines.push('```');
|
|
491
|
+
}
|
|
492
|
+
else {
|
|
493
|
+
catalogLines.push('### output');
|
|
494
|
+
catalogLines.push('```json');
|
|
495
|
+
catalogLines.push(stringifyStablePretty(testCase.output));
|
|
496
|
+
catalogLines.push('```');
|
|
497
|
+
}
|
|
498
|
+
catalogLines.push('');
|
|
499
|
+
}
|
|
500
|
+
catalogLines.push('');
|
|
501
|
+
(0, node_fs_1.writeFileSync)(catalogPath, `${catalogLines.join('\n')}\n`, 'utf8');
|
|
502
|
+
written.push(catalogPath);
|
|
503
|
+
catalogRows.push({
|
|
504
|
+
fileName: catalogFileName,
|
|
505
|
+
catalogId: catalog.catalogId,
|
|
506
|
+
title: catalog.title,
|
|
507
|
+
tests: catalog.cases.length
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
const indexPath = node_path_1.default.join(outDir, indexFileName);
|
|
511
|
+
const indexLines = [];
|
|
512
|
+
indexLines.push('# Unit Test Index');
|
|
513
|
+
indexLines.push('');
|
|
514
|
+
indexLines.push(`- catalogs: ${catalogRows.length}`);
|
|
515
|
+
indexLines.push('');
|
|
516
|
+
indexLines.push('## Catalog Files');
|
|
517
|
+
indexLines.push('');
|
|
518
|
+
for (const row of catalogRows.sort((a, b) => a.fileName.localeCompare(b.fileName))) {
|
|
519
|
+
indexLines.push(`- [${row.catalogId}](./${row.fileName})`);
|
|
520
|
+
indexLines.push(` - title: ${row.title}`);
|
|
521
|
+
indexLines.push(` - tests: ${row.tests}`);
|
|
522
|
+
}
|
|
523
|
+
indexLines.push('');
|
|
524
|
+
(0, node_fs_1.writeFileSync)(indexPath, `${indexLines.join('\n')}\n`, 'utf8');
|
|
525
|
+
written.push(indexPath);
|
|
526
|
+
return written;
|
|
527
|
+
}
|
|
528
|
+
function applyEvidenceOutputControls(report, options) {
|
|
529
|
+
const summaryOnly = Boolean(options.summaryOnly);
|
|
530
|
+
const limit = options.limit;
|
|
531
|
+
if (summaryOnly) {
|
|
532
|
+
return {
|
|
533
|
+
...report,
|
|
534
|
+
sqlCatalogs: [],
|
|
535
|
+
sqlCaseCatalogs: [],
|
|
536
|
+
testCaseCatalogs: [],
|
|
537
|
+
testCases: [],
|
|
538
|
+
display: {
|
|
539
|
+
summaryOnly: true,
|
|
540
|
+
limit,
|
|
541
|
+
truncated: report.sqlCatalogs.length > 0 ||
|
|
542
|
+
report.sqlCaseCatalogs.length > 0 ||
|
|
543
|
+
report.testCaseCatalogs.length > 0 ||
|
|
544
|
+
report.testCases.length > 0
|
|
545
|
+
}
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
if (limit === undefined) {
|
|
549
|
+
return report;
|
|
550
|
+
}
|
|
551
|
+
const limited = {
|
|
552
|
+
...report,
|
|
553
|
+
sqlCatalogs: report.sqlCatalogs.slice(0, limit),
|
|
554
|
+
sqlCaseCatalogs: report.sqlCaseCatalogs.slice(0, limit).map((catalog) => ({
|
|
555
|
+
...catalog,
|
|
556
|
+
cases: catalog.cases.slice(0, limit)
|
|
557
|
+
})),
|
|
558
|
+
testCaseCatalogs: report.testCaseCatalogs.slice(0, limit).map((catalog) => ({
|
|
559
|
+
...catalog,
|
|
560
|
+
cases: catalog.cases.slice(0, limit)
|
|
561
|
+
})),
|
|
562
|
+
testCases: report.testCases.slice(0, limit),
|
|
563
|
+
display: {
|
|
564
|
+
summaryOnly: false,
|
|
565
|
+
limit,
|
|
566
|
+
truncated: report.sqlCatalogs.length > limit ||
|
|
567
|
+
report.sqlCaseCatalogs.length > limit ||
|
|
568
|
+
report.testCaseCatalogs.length > limit ||
|
|
569
|
+
report.testCases.length > limit ||
|
|
570
|
+
report.sqlCaseCatalogs.some((catalog) => catalog.cases.length > limit) ||
|
|
571
|
+
report.testCaseCatalogs.some((catalog) => catalog.cases.length > limit)
|
|
572
|
+
}
|
|
573
|
+
};
|
|
574
|
+
return limited;
|
|
575
|
+
}
|
|
576
|
+
function normalizeEvidenceLimit(value) {
|
|
577
|
+
if (value === undefined) {
|
|
578
|
+
return undefined;
|
|
579
|
+
}
|
|
580
|
+
const parsed = Number(value);
|
|
581
|
+
if (!Number.isInteger(parsed) || parsed <= 0) {
|
|
582
|
+
throw new TestEvidenceRuntimeError(`Unsupported limit: ${value}. Use a positive integer.`);
|
|
583
|
+
}
|
|
584
|
+
return parsed;
|
|
585
|
+
}
|
|
586
|
+
function findCatalogDefinitionPath(report, catalogId) {
|
|
587
|
+
const functionCatalog = report.testCaseCatalogs.find((catalog) => catalog.id === catalogId);
|
|
588
|
+
if (functionCatalog === null || functionCatalog === void 0 ? void 0 : functionCatalog.definitionPath) {
|
|
589
|
+
return functionCatalog.definitionPath;
|
|
590
|
+
}
|
|
591
|
+
const sqlCatalog = report.sqlCaseCatalogs.find((catalog) => catalog.id === catalogId);
|
|
592
|
+
if (sqlCatalog === null || sqlCatalog === void 0 ? void 0 : sqlCatalog.definitionPath) {
|
|
593
|
+
return sqlCatalog.definitionPath;
|
|
594
|
+
}
|
|
595
|
+
return undefined;
|
|
596
|
+
}
|
|
597
|
+
function toSpecificationSlug(definition) {
|
|
598
|
+
if (definition === '(unknown)') {
|
|
599
|
+
return 'unknown';
|
|
600
|
+
}
|
|
601
|
+
const normalized = definition
|
|
602
|
+
.replace(/\\/g, '/')
|
|
603
|
+
.replace(/^\//, '')
|
|
604
|
+
.replace(/[^a-zA-Z0-9/_-]+/g, '-')
|
|
605
|
+
.replace(/\/+/g, '__')
|
|
606
|
+
.replace(/-+/g, '-')
|
|
607
|
+
.replace(/^[-_]+|[-_]+$/g, '');
|
|
608
|
+
return normalized || 'unknown';
|
|
609
|
+
}
|
|
610
|
+
function formatDefinitionLinkMarkdown(definitionPath, options) {
|
|
611
|
+
if (!definitionPath) {
|
|
612
|
+
return '(unknown)';
|
|
613
|
+
}
|
|
614
|
+
const normalizedPath = definitionPath.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
615
|
+
if (!normalizedPath) {
|
|
616
|
+
return '(unknown)';
|
|
617
|
+
}
|
|
618
|
+
if ((options === null || options === void 0 ? void 0 : options.mode) === 'github' && options.github) {
|
|
619
|
+
const serverUrl = options.github.serverUrl.replace(/\/+$/, '');
|
|
620
|
+
const repository = options.github.repository.trim();
|
|
621
|
+
const ref = options.github.ref.trim();
|
|
622
|
+
if (serverUrl && repository && ref) {
|
|
623
|
+
const encodedPath = normalizedPath.split('/').map((part) => encodeURIComponent(part)).join('/');
|
|
624
|
+
const url = `${serverUrl}/${repository}/blob/${encodeURIComponent(ref)}/${encodedPath}`;
|
|
625
|
+
return `[${normalizedPath}](${url})`;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if ((options === null || options === void 0 ? void 0 : options.mode) === 'path' && options.path) {
|
|
629
|
+
const absoluteTarget = node_path_1.default.resolve(options.path.sourceRootDir, normalizedPath);
|
|
630
|
+
const absoluteMarkdownDir = node_path_1.default.resolve(options.path.markdownDir);
|
|
631
|
+
const relativePath = node_path_1.default.relative(absoluteMarkdownDir, absoluteTarget).replace(/\\/g, '/');
|
|
632
|
+
return `[${normalizedPath}](${relativePath || normalizedPath})`;
|
|
633
|
+
}
|
|
634
|
+
return `[${normalizedPath}](${normalizedPath})`;
|
|
635
|
+
}
|
|
636
|
+
function materializeEvidenceForRef(args) {
|
|
637
|
+
const toReport = (rootDir) => {
|
|
638
|
+
try {
|
|
639
|
+
return applyEvidenceOutputControls(runTestEvidenceSpecification({
|
|
640
|
+
mode: 'specification',
|
|
641
|
+
rootDir,
|
|
642
|
+
specsDir: args.specsDir,
|
|
643
|
+
testsDir: args.testsDir,
|
|
644
|
+
specModule: args.specModule
|
|
645
|
+
}), {
|
|
646
|
+
summaryOnly: args.summaryOnly,
|
|
647
|
+
limit: args.limit
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
catch (error) {
|
|
651
|
+
if (error instanceof TestEvidenceRuntimeError &&
|
|
652
|
+
error.code === 'NO_SPECS_FOUND') {
|
|
653
|
+
return createEmptySpecificationEvidence();
|
|
654
|
+
}
|
|
655
|
+
throw error;
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
if (args.allowCurrentWorkspace) {
|
|
659
|
+
const report = toReport(args.repoRoot);
|
|
660
|
+
return {
|
|
661
|
+
sha: args.resolvedSha,
|
|
662
|
+
report,
|
|
663
|
+
previewJsonPath: writePreviewSnapshot(args.repoRoot, report)
|
|
664
|
+
};
|
|
665
|
+
}
|
|
666
|
+
const worktreeDir = (0, node_fs_1.mkdtempSync)(node_path_1.default.join(args.tempRoot, 'wt-'));
|
|
667
|
+
args.createdWorktrees.push(worktreeDir);
|
|
668
|
+
runGitCommand(args.repoRoot, ['worktree', 'add', '--detach', worktreeDir, args.resolvedSha]);
|
|
669
|
+
const report = toReport(worktreeDir);
|
|
670
|
+
return {
|
|
671
|
+
sha: args.resolvedSha,
|
|
672
|
+
report,
|
|
673
|
+
previewJsonPath: writePreviewSnapshot(worktreeDir, report)
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
function createEmptySpecificationEvidence() {
|
|
677
|
+
return {
|
|
678
|
+
schemaVersion: 1,
|
|
679
|
+
mode: 'specification',
|
|
680
|
+
summary: {
|
|
681
|
+
sqlCatalogCount: 0,
|
|
682
|
+
sqlCaseCatalogCount: 0,
|
|
683
|
+
testCaseCount: 0,
|
|
684
|
+
specFilesScanned: 0,
|
|
685
|
+
testFilesScanned: 0
|
|
686
|
+
},
|
|
687
|
+
sqlCatalogs: [],
|
|
688
|
+
sqlCaseCatalogs: [],
|
|
689
|
+
testCaseCatalogs: [],
|
|
690
|
+
testCases: []
|
|
691
|
+
};
|
|
692
|
+
}
|
|
693
|
+
function writePreviewSnapshot(rootDir, report) {
|
|
694
|
+
const previewDir = node_path_1.default.resolve(rootDir, 'artifacts', 'test-evidence');
|
|
695
|
+
(0, node_fs_1.mkdirSync)(previewDir, { recursive: true });
|
|
696
|
+
const previewPath = node_path_1.default.join(previewDir, 'test-specification.preview.json');
|
|
697
|
+
(0, node_fs_1.writeFileSync)(previewPath, `${JSON.stringify(report, null, 2)}\n`, 'utf8');
|
|
698
|
+
return previewPath;
|
|
699
|
+
}
|
|
700
|
+
function cleanupWorktrees(repoRoot, worktreeDirs) {
|
|
701
|
+
for (const worktreeDir of [...worktreeDirs].reverse()) {
|
|
702
|
+
try {
|
|
703
|
+
runGitCommand(repoRoot, ['worktree', 'remove', '--force', worktreeDir]);
|
|
704
|
+
}
|
|
705
|
+
catch {
|
|
706
|
+
(0, node_fs_1.rmSync)(worktreeDir, { recursive: true, force: true });
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
function runGitCommand(repoRoot, args) {
|
|
711
|
+
try {
|
|
712
|
+
return (0, node_child_process_1.execFileSync)('git', args, {
|
|
713
|
+
cwd: repoRoot,
|
|
714
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
715
|
+
encoding: 'utf8'
|
|
716
|
+
}).trim();
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
720
|
+
throw new TestEvidenceRuntimeError(`Git command failed: git ${args.join(' ')} (${message})`);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
function resolveGitSha(repoRoot, ref) {
|
|
724
|
+
return runGitCommand(repoRoot, ['rev-parse', ref]);
|
|
725
|
+
}
|
|
726
|
+
function resolveGitMergeBase(repoRoot, baseRef, headRef) {
|
|
727
|
+
return runGitCommand(repoRoot, ['merge-base', baseRef, headRef]);
|
|
728
|
+
}
|
|
729
|
+
function toSqlEvidence(rootDir, loaded) {
|
|
730
|
+
var _a, _b, _c;
|
|
731
|
+
const id = typeof loaded.spec.id === 'string' && loaded.spec.id.trim().length > 0
|
|
732
|
+
? loaded.spec.id.trim()
|
|
733
|
+
: `<missing-id:${node_path_1.default.basename(loaded.filePath)}>`;
|
|
734
|
+
const sqlFile = typeof loaded.spec.sqlFile === 'string' && loaded.spec.sqlFile.trim().length > 0
|
|
735
|
+
? loaded.spec.sqlFile.trim()
|
|
736
|
+
: null;
|
|
737
|
+
const paramsShape = ((_a = loaded.spec.params) === null || _a === void 0 ? void 0 : _a.shape) === 'named' || ((_b = loaded.spec.params) === null || _b === void 0 ? void 0 : _b.shape) === 'positional'
|
|
738
|
+
? loaded.spec.params.shape
|
|
739
|
+
: 'unknown';
|
|
740
|
+
const resolvedSqlPath = sqlFile ? node_path_1.default.resolve(node_path_1.default.dirname(loaded.filePath), sqlFile) : null;
|
|
741
|
+
const specFile = normalizePath(node_path_1.default.relative(rootDir, loaded.filePath));
|
|
742
|
+
return {
|
|
743
|
+
kind: 'sql-catalog',
|
|
744
|
+
id,
|
|
745
|
+
specFile,
|
|
746
|
+
sqlFile,
|
|
747
|
+
sqlFileResolved: Boolean(resolvedSqlPath && (0, node_fs_1.existsSync)(resolvedSqlPath)),
|
|
748
|
+
paramsShape,
|
|
749
|
+
hasOutputMapping: ((_c = loaded.spec.output) === null || _c === void 0 ? void 0 : _c.mapping) !== undefined
|
|
750
|
+
};
|
|
751
|
+
}
|
|
752
|
+
function loadEvidenceModule(rootDir, testsDir, specModule) {
|
|
753
|
+
const moduleCandidates = resolveEvidenceModuleCandidates(rootDir, testsDir, specModule);
|
|
754
|
+
const target = moduleCandidates.find((candidate) => (0, node_fs_1.existsSync)(candidate));
|
|
755
|
+
if (!target) {
|
|
756
|
+
return undefined;
|
|
757
|
+
}
|
|
758
|
+
try {
|
|
759
|
+
const requireFn = (0, node_module_1.createRequire)(__filename);
|
|
760
|
+
const loaded = requireFn(target);
|
|
761
|
+
const normalized = (loaded === null || loaded === void 0 ? void 0 : loaded.default) && (0, sqlCatalogDiscovery_1.isPlainObject)(loaded.default)
|
|
762
|
+
? loaded.default
|
|
763
|
+
: loaded;
|
|
764
|
+
return normalized;
|
|
765
|
+
}
|
|
766
|
+
catch (error) {
|
|
767
|
+
throw new TestEvidenceRuntimeError(`Failed to load evidence module ${target}: ${error instanceof Error ? error.message : String(error)}`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
function resolveEvidenceModuleCandidates(rootDir, testsDir, specModule) {
|
|
771
|
+
if (specModule) {
|
|
772
|
+
const absolute = node_path_1.default.resolve(rootDir, specModule);
|
|
773
|
+
return [
|
|
774
|
+
absolute,
|
|
775
|
+
`${absolute}.js`,
|
|
776
|
+
`${absolute}.cjs`,
|
|
777
|
+
`${absolute}.ts`
|
|
778
|
+
];
|
|
779
|
+
}
|
|
780
|
+
const base = node_path_1.default.resolve(testsDir, 'specs', 'index');
|
|
781
|
+
return [
|
|
782
|
+
`${base}.js`,
|
|
783
|
+
`${base}.cjs`,
|
|
784
|
+
`${base}.ts`
|
|
785
|
+
];
|
|
786
|
+
}
|
|
787
|
+
function readTestCaseCatalogsFromModule(moduleValue) {
|
|
788
|
+
if (!Array.isArray(moduleValue.testCaseCatalogs)) {
|
|
789
|
+
return [];
|
|
790
|
+
}
|
|
791
|
+
return moduleValue.testCaseCatalogs.map((catalog, index) => {
|
|
792
|
+
if (!(0, sqlCatalogDiscovery_1.isPlainObject)(catalog)) {
|
|
793
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}] must be an object in evidence module.`);
|
|
794
|
+
}
|
|
795
|
+
const id = typeof catalog.id === 'string' ? catalog.id.trim() : '';
|
|
796
|
+
const title = typeof catalog.title === 'string' ? catalog.title.trim() : '';
|
|
797
|
+
const cases = Array.isArray(catalog.cases) ? catalog.cases : [];
|
|
798
|
+
if (!id || !title) {
|
|
799
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}] requires non-empty id/title.`);
|
|
800
|
+
}
|
|
801
|
+
const normalizedCases = cases.map((item, caseIndex) => {
|
|
802
|
+
if (!(0, sqlCatalogDiscovery_1.isPlainObject)(item)) {
|
|
803
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] must be an object.`);
|
|
804
|
+
}
|
|
805
|
+
const caseId = typeof item.id === 'string' ? item.id.trim() : '';
|
|
806
|
+
const caseTitle = typeof item.title === 'string' ? item.title.trim() : '';
|
|
807
|
+
if (!caseId || !caseTitle) {
|
|
808
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] requires non-empty id/title.`);
|
|
809
|
+
}
|
|
810
|
+
const description = typeof item.description === 'string' && item.description.trim().length > 0
|
|
811
|
+
? item.description.trim()
|
|
812
|
+
: undefined;
|
|
813
|
+
const hasDirectInput = Object.prototype.hasOwnProperty.call(item, 'input');
|
|
814
|
+
const hasDirectOutput = Object.prototype.hasOwnProperty.call(item, 'output');
|
|
815
|
+
const hasDirectExpected = Object.prototype.hasOwnProperty.call(item, 'expected');
|
|
816
|
+
const hasDirectError = Object.prototype.hasOwnProperty.call(item, 'error');
|
|
817
|
+
const hasDirectTags = Object.prototype.hasOwnProperty.call(item, 'tags');
|
|
818
|
+
const hasDirectFocus = Object.prototype.hasOwnProperty.call(item, 'focus');
|
|
819
|
+
const hasDirectRefs = Object.prototype.hasOwnProperty.call(item, 'refs');
|
|
820
|
+
const evidenceValue = Object.prototype.hasOwnProperty.call(item, 'evidence') ? item.evidence : undefined;
|
|
821
|
+
const hasEvidence = (0, sqlCatalogDiscovery_1.isPlainObject)(evidenceValue);
|
|
822
|
+
// Accept both normalized evidence shape and raw test catalog shape (`evidence.*`).
|
|
823
|
+
const input = hasDirectInput
|
|
824
|
+
? item.input
|
|
825
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'input')
|
|
826
|
+
? evidenceValue.input
|
|
827
|
+
: undefined;
|
|
828
|
+
const output = hasDirectOutput
|
|
829
|
+
? item.output
|
|
830
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'output')
|
|
831
|
+
? evidenceValue.output
|
|
832
|
+
: undefined;
|
|
833
|
+
const expectedRaw = hasDirectExpected
|
|
834
|
+
? item.expected
|
|
835
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'expected')
|
|
836
|
+
? evidenceValue.expected
|
|
837
|
+
: undefined;
|
|
838
|
+
const expected = expectedRaw === 'success' || expectedRaw === 'throws' || expectedRaw === 'errorResult'
|
|
839
|
+
? expectedRaw
|
|
840
|
+
: output === undefined
|
|
841
|
+
? undefined
|
|
842
|
+
: 'success';
|
|
843
|
+
const errorRaw = hasDirectError
|
|
844
|
+
? item.error
|
|
845
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'error')
|
|
846
|
+
? evidenceValue.error
|
|
847
|
+
: undefined;
|
|
848
|
+
const tagsRaw = hasDirectTags
|
|
849
|
+
? item.tags
|
|
850
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'tags')
|
|
851
|
+
? evidenceValue.tags
|
|
852
|
+
: undefined;
|
|
853
|
+
const focusRaw = hasDirectFocus
|
|
854
|
+
? item.focus
|
|
855
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'focus')
|
|
856
|
+
? evidenceValue.focus
|
|
857
|
+
: undefined;
|
|
858
|
+
const refsRaw = hasDirectRefs
|
|
859
|
+
? item.refs
|
|
860
|
+
: hasEvidence && Object.prototype.hasOwnProperty.call(evidenceValue, 'refs')
|
|
861
|
+
? evidenceValue.refs
|
|
862
|
+
: undefined;
|
|
863
|
+
if (input === undefined || expected === undefined) {
|
|
864
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] requires input/expected evidence fields.`);
|
|
865
|
+
}
|
|
866
|
+
if (expected !== 'throws' && output === undefined) {
|
|
867
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] requires output when expected is not "throws".`);
|
|
868
|
+
}
|
|
869
|
+
if (expected === 'throws') {
|
|
870
|
+
if (!(0, sqlCatalogDiscovery_1.isPlainObject)(errorRaw)) {
|
|
871
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] requires error when expected is "throws".`);
|
|
872
|
+
}
|
|
873
|
+
if (typeof errorRaw.name !== 'string' ||
|
|
874
|
+
typeof errorRaw.message !== 'string' ||
|
|
875
|
+
(errorRaw.match !== 'equals' && errorRaw.match !== 'contains')) {
|
|
876
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}].error must define name/message/match.`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const normalizedTags = Array.isArray(tagsRaw) && tagsRaw.every((tag) => typeof tag === 'string' && tag.trim().length > 0)
|
|
880
|
+
? normalizeCaseTags(tagsRaw.map((tag) => tag.trim()))
|
|
881
|
+
: undefined;
|
|
882
|
+
const normalizedFocus = typeof focusRaw === 'string' && focusRaw.trim().length > 0
|
|
883
|
+
? focusRaw.trim()
|
|
884
|
+
: undefined;
|
|
885
|
+
const normalizedRefs = normalizeCatalogRefs(refsRaw);
|
|
886
|
+
if (!normalizedTags || normalizedTags.length !== 2) {
|
|
887
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] requires tags with exactly 2 axes: [intent, technique].`);
|
|
888
|
+
}
|
|
889
|
+
if (!normalizedFocus) {
|
|
890
|
+
throw new TestEvidenceRuntimeError(`testCaseCatalogs[${index}].cases[${caseIndex}] requires focus sentence.`);
|
|
891
|
+
}
|
|
892
|
+
return {
|
|
893
|
+
id: caseId,
|
|
894
|
+
title: caseTitle,
|
|
895
|
+
...(description ? { description } : {}),
|
|
896
|
+
input,
|
|
897
|
+
expected,
|
|
898
|
+
...(expected === 'throws'
|
|
899
|
+
? {
|
|
900
|
+
error: {
|
|
901
|
+
name: errorRaw.name,
|
|
902
|
+
message: errorRaw.message,
|
|
903
|
+
match: errorRaw.match
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
: { output }),
|
|
907
|
+
tags: normalizedTags,
|
|
908
|
+
focus: normalizedFocus,
|
|
909
|
+
...(normalizedRefs.length > 0 ? { refs: normalizedRefs } : {})
|
|
910
|
+
};
|
|
911
|
+
});
|
|
912
|
+
const description = typeof catalog.description === 'string' && catalog.description.trim().length > 0
|
|
913
|
+
? catalog.description.trim()
|
|
914
|
+
: undefined;
|
|
915
|
+
const definitionPath = typeof catalog.definitionPath === 'string' && catalog.definitionPath.trim().length > 0
|
|
916
|
+
? catalog.definitionPath.trim()
|
|
917
|
+
: undefined;
|
|
918
|
+
const refs = normalizeCatalogRefs(catalog.refs);
|
|
919
|
+
return {
|
|
920
|
+
id,
|
|
921
|
+
title,
|
|
922
|
+
...(description ? { description } : {}),
|
|
923
|
+
...(definitionPath ? { definitionPath } : {}),
|
|
924
|
+
...(refs.length > 0 ? { refs } : {}),
|
|
925
|
+
cases: normalizedCases
|
|
926
|
+
};
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
function flattenTestCaseCatalogs(catalogs) {
|
|
930
|
+
const rows = [];
|
|
931
|
+
for (const catalog of catalogs) {
|
|
932
|
+
for (const testCase of catalog.cases) {
|
|
933
|
+
rows.push({
|
|
934
|
+
kind: 'test-case',
|
|
935
|
+
id: `${catalog.id}.${testCase.id}`,
|
|
936
|
+
catalogId: catalog.id,
|
|
937
|
+
caseId: testCase.id,
|
|
938
|
+
filePath: 'tests/specs/index',
|
|
939
|
+
title: testCase.title,
|
|
940
|
+
...(testCase.description ? { description: testCase.description } : {})
|
|
941
|
+
});
|
|
942
|
+
}
|
|
943
|
+
}
|
|
944
|
+
return rows;
|
|
945
|
+
}
|
|
946
|
+
function readSqlCaseCatalogsFromModule(moduleValue) {
|
|
947
|
+
if (!Array.isArray(moduleValue.sqlCatalogCases)) {
|
|
948
|
+
return [];
|
|
949
|
+
}
|
|
950
|
+
return moduleValue.sqlCatalogCases.map((catalog, index) => normalizeSqlCaseCatalog(catalog, index));
|
|
951
|
+
}
|
|
952
|
+
function normalizeSqlCaseCatalog(catalog, index) {
|
|
953
|
+
if (!(0, sqlCatalogDiscovery_1.isPlainObject)(catalog)) {
|
|
954
|
+
throw new TestEvidenceRuntimeError(`sqlCatalogCases[${index}] must be an object in evidence module.`);
|
|
955
|
+
}
|
|
956
|
+
const id = typeof catalog.id === 'string' ? catalog.id.trim() : '';
|
|
957
|
+
const title = typeof catalog.title === 'string' ? catalog.title.trim() : '';
|
|
958
|
+
if (!id || !title) {
|
|
959
|
+
throw new TestEvidenceRuntimeError(`sqlCatalogCases[${index}] requires non-empty id/title.`);
|
|
960
|
+
}
|
|
961
|
+
const details = (0, sqlCatalogDiscovery_1.isPlainObject)(catalog.catalog) ? catalog.catalog : {};
|
|
962
|
+
const params = (0, sqlCatalogDiscovery_1.isPlainObject)(details.params) ? details.params : {};
|
|
963
|
+
const output = (0, sqlCatalogDiscovery_1.isPlainObject)(details.output) ? details.output : {};
|
|
964
|
+
const mapping = (0, sqlCatalogDiscovery_1.isPlainObject)(output.mapping) ? output.mapping : {};
|
|
965
|
+
const columnMapRaw = (0, sqlCatalogDiscovery_1.isPlainObject)(mapping.columnMap) ? mapping.columnMap : {};
|
|
966
|
+
const columnMap = Object.fromEntries(Object.entries(columnMapRaw)
|
|
967
|
+
.filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
|
|
968
|
+
.sort((a, b) => a[0].localeCompare(b[0])));
|
|
969
|
+
const sql = typeof details.sql === 'string' ? details.sql : '';
|
|
970
|
+
const example = (0, sqlCatalogDiscovery_1.isPlainObject)(params.example) ? { ...params.example } : {};
|
|
971
|
+
const fixturesRaw = Array.isArray(catalog.fixtures) ? catalog.fixtures : [];
|
|
972
|
+
const fixtures = fixturesRaw
|
|
973
|
+
.filter((item) => (0, sqlCatalogDiscovery_1.isPlainObject)(item) && typeof item.tableName === 'string')
|
|
974
|
+
.map((item) => ({
|
|
975
|
+
tableName: item.tableName,
|
|
976
|
+
...((0, sqlCatalogDiscovery_1.isPlainObject)(item.schema) && (0, sqlCatalogDiscovery_1.isPlainObject)(item.schema.columns)
|
|
977
|
+
? {
|
|
978
|
+
schema: {
|
|
979
|
+
columns: Object.fromEntries(Object.entries(item.schema.columns)
|
|
980
|
+
.filter((entry) => typeof entry[0] === 'string' && typeof entry[1] === 'string')
|
|
981
|
+
.sort((a, b) => a[0].localeCompare(b[0])))
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
: {}),
|
|
985
|
+
rowsCount: Array.isArray(item.rows) ? item.rows.length : typeof item.rowsCount === 'number' ? item.rowsCount : 0
|
|
986
|
+
}))
|
|
987
|
+
.sort((a, b) => a.tableName.localeCompare(b.tableName));
|
|
988
|
+
const casesRaw = Array.isArray(catalog.cases) ? catalog.cases : [];
|
|
989
|
+
const baseParams = (0, sqlCatalogDiscovery_1.isPlainObject)(params.example) ? { ...params.example } : {};
|
|
990
|
+
const cases = casesRaw
|
|
991
|
+
.filter((item) => (0, sqlCatalogDiscovery_1.isPlainObject)(item) && typeof item.id === 'string' && typeof item.title === 'string')
|
|
992
|
+
.map((item, caseIndex) => {
|
|
993
|
+
const arrangedParams = resolveCaseArrangeParams(item, index, caseIndex);
|
|
994
|
+
const mergedParams = arrangedParams ? { ...baseParams, ...arrangedParams } : { ...baseParams };
|
|
995
|
+
const expected = Array.isArray(item.expected) ? [...item.expected] : [];
|
|
996
|
+
return {
|
|
997
|
+
id: item.id,
|
|
998
|
+
title: item.title,
|
|
999
|
+
params: Object.fromEntries(Object.entries(mergedParams)
|
|
1000
|
+
.filter((entry) => typeof entry[0] === 'string')
|
|
1001
|
+
.sort((a, b) => a[0].localeCompare(b[0]))),
|
|
1002
|
+
expected
|
|
1003
|
+
};
|
|
1004
|
+
})
|
|
1005
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1006
|
+
const nestedDefinitionPath = typeof details.definitionPath === 'string' && details.definitionPath.trim().length > 0
|
|
1007
|
+
? details.definitionPath.trim()
|
|
1008
|
+
: undefined;
|
|
1009
|
+
const description = typeof catalog.description === 'string' && catalog.description.trim().length > 0
|
|
1010
|
+
? catalog.description.trim()
|
|
1011
|
+
: undefined;
|
|
1012
|
+
const definitionPath = typeof catalog.definitionPath === 'string' && catalog.definitionPath.trim().length > 0
|
|
1013
|
+
? catalog.definitionPath.trim()
|
|
1014
|
+
: nestedDefinitionPath;
|
|
1015
|
+
return {
|
|
1016
|
+
id,
|
|
1017
|
+
title,
|
|
1018
|
+
...(description ? { description } : {}),
|
|
1019
|
+
...(definitionPath ? { definitionPath } : {}),
|
|
1020
|
+
params: {
|
|
1021
|
+
shape: 'named',
|
|
1022
|
+
example
|
|
1023
|
+
},
|
|
1024
|
+
output: {
|
|
1025
|
+
mapping: {
|
|
1026
|
+
columnMap
|
|
1027
|
+
}
|
|
1028
|
+
},
|
|
1029
|
+
// Keep SQL text unchanged so evidence remains a direct projection of test source.
|
|
1030
|
+
sql,
|
|
1031
|
+
fixtures,
|
|
1032
|
+
cases
|
|
1033
|
+
};
|
|
1034
|
+
}
|
|
1035
|
+
function resolveCaseArrangeParams(testCase, catalogIndex, caseIndex) {
|
|
1036
|
+
const arrange = testCase.arrange;
|
|
1037
|
+
if (arrange === undefined) {
|
|
1038
|
+
return undefined;
|
|
1039
|
+
}
|
|
1040
|
+
if (typeof arrange !== 'function') {
|
|
1041
|
+
throw new TestEvidenceRuntimeError(`sqlCatalogCases[${catalogIndex}].cases[${caseIndex}].arrange must be a function when provided.`);
|
|
1042
|
+
}
|
|
1043
|
+
const arranged = arrange();
|
|
1044
|
+
if (!(0, sqlCatalogDiscovery_1.isPlainObject)(arranged)) {
|
|
1045
|
+
throw new TestEvidenceRuntimeError(`sqlCatalogCases[${catalogIndex}].cases[${caseIndex}].arrange must return an object.`);
|
|
1046
|
+
}
|
|
1047
|
+
return arranged;
|
|
1048
|
+
}
|
|
1049
|
+
function loadTestCaseCatalogEvidence(rootDir, filePath) {
|
|
1050
|
+
let parsed;
|
|
1051
|
+
try {
|
|
1052
|
+
parsed = JSON.parse((0, node_fs_1.readFileSync)(filePath, 'utf8'));
|
|
1053
|
+
}
|
|
1054
|
+
catch (error) {
|
|
1055
|
+
throw new TestEvidenceRuntimeError(`Failed to parse test-case catalog file ${filePath}: ${error instanceof Error ? error.message : String(error)}`);
|
|
1056
|
+
}
|
|
1057
|
+
if (!(0, sqlCatalogDiscovery_1.isPlainObject)(parsed)) {
|
|
1058
|
+
throw new TestEvidenceRuntimeError(`Test-case catalog file must contain an object: ${filePath}`);
|
|
1059
|
+
}
|
|
1060
|
+
const document = parsed;
|
|
1061
|
+
if (!Array.isArray(document.catalogs)) {
|
|
1062
|
+
throw new TestEvidenceRuntimeError(`Test-case catalog file must define a catalogs array: ${filePath}`);
|
|
1063
|
+
}
|
|
1064
|
+
const relative = normalizePath(node_path_1.default.relative(rootDir, filePath));
|
|
1065
|
+
const rows = [];
|
|
1066
|
+
const catalogs = document.catalogs;
|
|
1067
|
+
for (const catalog of catalogs) {
|
|
1068
|
+
const catalogId = typeof catalog.id === 'string' ? catalog.id.trim() : '';
|
|
1069
|
+
if (!catalogId) {
|
|
1070
|
+
throw new TestEvidenceRuntimeError(`Test-case catalog id must be a non-empty string: ${filePath}`);
|
|
1071
|
+
}
|
|
1072
|
+
if (!Array.isArray(catalog.cases)) {
|
|
1073
|
+
throw new TestEvidenceRuntimeError(`Test-case catalog "${catalogId}" must define a cases array: ${filePath}`);
|
|
1074
|
+
}
|
|
1075
|
+
const cases = catalog.cases;
|
|
1076
|
+
for (const testCase of cases) {
|
|
1077
|
+
const caseId = typeof testCase.id === 'string' ? testCase.id.trim() : '';
|
|
1078
|
+
const title = typeof testCase.title === 'string' ? testCase.title.trim() : '';
|
|
1079
|
+
if (!caseId || !title) {
|
|
1080
|
+
throw new TestEvidenceRuntimeError(`Test-case catalog "${catalogId}" requires each case to define non-empty id/title: ${filePath}`);
|
|
1081
|
+
}
|
|
1082
|
+
const description = typeof testCase.description === 'string' && testCase.description.trim().length > 0
|
|
1083
|
+
? testCase.description.trim()
|
|
1084
|
+
: undefined;
|
|
1085
|
+
rows.push({
|
|
1086
|
+
kind: 'test-case',
|
|
1087
|
+
id: `${catalogId}.${caseId}`,
|
|
1088
|
+
catalogId,
|
|
1089
|
+
caseId,
|
|
1090
|
+
filePath: relative,
|
|
1091
|
+
title,
|
|
1092
|
+
...(description ? { description } : {})
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
return rows;
|
|
1097
|
+
}
|
|
1098
|
+
function walkFiles(rootDir, predicate) {
|
|
1099
|
+
const stack = [rootDir];
|
|
1100
|
+
const files = [];
|
|
1101
|
+
while (stack.length > 0) {
|
|
1102
|
+
const current = stack.pop();
|
|
1103
|
+
const entries = (0, node_fs_1.readdirSync)(current, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name));
|
|
1104
|
+
for (const entry of entries) {
|
|
1105
|
+
const absolute = node_path_1.default.join(current, entry.name);
|
|
1106
|
+
if (entry.isDirectory()) {
|
|
1107
|
+
stack.push(absolute);
|
|
1108
|
+
continue;
|
|
1109
|
+
}
|
|
1110
|
+
if (entry.isFile() && predicate(absolute)) {
|
|
1111
|
+
files.push(absolute);
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
1116
|
+
}
|
|
1117
|
+
function isTestCaseCatalogFile(filePath) {
|
|
1118
|
+
const lowered = filePath.toLowerCase();
|
|
1119
|
+
return lowered.endsWith('.test-case-catalog.json');
|
|
1120
|
+
}
|
|
1121
|
+
function normalizePath(input) {
|
|
1122
|
+
return input.split(node_path_1.default.sep).join('/');
|
|
1123
|
+
}
|
|
1124
|
+
function normalizeCatalogRefs(value) {
|
|
1125
|
+
if (!Array.isArray(value)) {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
return value
|
|
1129
|
+
.filter((item) => (0, sqlCatalogDiscovery_1.isPlainObject)(item))
|
|
1130
|
+
.map((item) => ({
|
|
1131
|
+
label: typeof item.label === 'string' ? item.label.trim() : '',
|
|
1132
|
+
url: typeof item.url === 'string' ? item.url.trim() : ''
|
|
1133
|
+
}))
|
|
1134
|
+
.filter((item) => item.label.length > 0 && item.url.length > 0)
|
|
1135
|
+
.sort((a, b) => a.label.localeCompare(b.label) || a.url.localeCompare(b.url));
|
|
1136
|
+
}
|
|
1137
|
+
const TAG_NORMALIZATION_MAP = {
|
|
1138
|
+
boundary: 'bva'
|
|
1139
|
+
};
|
|
1140
|
+
const INTENT_TAGS = ['normalization', 'validation', 'authorization', 'invariant'];
|
|
1141
|
+
const TECHNIQUE_TAGS = ['ep', 'bva', 'idempotence', 'state'];
|
|
1142
|
+
const INTENT_TAG_SET = new Set(INTENT_TAGS);
|
|
1143
|
+
const TECHNIQUE_TAG_SET = new Set(TECHNIQUE_TAGS);
|
|
1144
|
+
function normalizeCaseTags(tags) {
|
|
1145
|
+
var _a;
|
|
1146
|
+
const normalized = new Set();
|
|
1147
|
+
for (const rawTag of tags) {
|
|
1148
|
+
const lowered = rawTag.trim().toLowerCase();
|
|
1149
|
+
const mapped = (_a = TAG_NORMALIZATION_MAP[lowered]) !== null && _a !== void 0 ? _a : lowered;
|
|
1150
|
+
if (INTENT_TAG_SET.has(mapped) || TECHNIQUE_TAG_SET.has(mapped)) {
|
|
1151
|
+
normalized.add(mapped);
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
const intent = INTENT_TAGS.find((tag) => normalized.has(tag));
|
|
1155
|
+
const technique = TECHNIQUE_TAGS.find((tag) => normalized.has(tag));
|
|
1156
|
+
const result = [];
|
|
1157
|
+
if (intent) {
|
|
1158
|
+
result.push(intent);
|
|
1159
|
+
}
|
|
1160
|
+
if (technique) {
|
|
1161
|
+
result.push(technique);
|
|
1162
|
+
}
|
|
1163
|
+
return result;
|
|
1164
|
+
}
|
|
1165
|
+
function formatCaseTags(tags) {
|
|
1166
|
+
const normalized = Array.isArray(tags) ? normalizeCaseTags(tags) : [];
|
|
1167
|
+
return `[${normalized.join(', ')}]`;
|
|
1168
|
+
}
|
|
1169
|
+
function formatCaseFocus(focus) {
|
|
1170
|
+
if (typeof focus === 'string' && focus.trim().length > 0) {
|
|
1171
|
+
return focus.trim();
|
|
1172
|
+
}
|
|
1173
|
+
return '(not specified)';
|
|
1174
|
+
}
|
|
1175
|
+
function stringifyStablePretty(value) {
|
|
1176
|
+
var _a;
|
|
1177
|
+
return (_a = JSON.stringify(sortDeep(value), null, 2)) !== null && _a !== void 0 ? _a : 'null';
|
|
1178
|
+
}
|
|
1179
|
+
function stringifyErrorPretty(value) {
|
|
1180
|
+
var _a;
|
|
1181
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
1182
|
+
return 'null';
|
|
1183
|
+
}
|
|
1184
|
+
const source = value;
|
|
1185
|
+
return (_a = JSON.stringify({
|
|
1186
|
+
name: source.name,
|
|
1187
|
+
message: source.message,
|
|
1188
|
+
match: source.match
|
|
1189
|
+
}, null, 2)) !== null && _a !== void 0 ? _a : 'null';
|
|
1190
|
+
}
|
|
1191
|
+
function sortDeep(value) {
|
|
1192
|
+
if (Array.isArray(value)) {
|
|
1193
|
+
return value.map((item) => sortDeep(item));
|
|
1194
|
+
}
|
|
1195
|
+
if (value !== null && typeof value === 'object') {
|
|
1196
|
+
const entries = Object.entries(value)
|
|
1197
|
+
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
1198
|
+
.map(([key, nested]) => [key, sortDeep(nested)]);
|
|
1199
|
+
return Object.fromEntries(entries);
|
|
1200
|
+
}
|
|
1201
|
+
return value;
|
|
1202
|
+
}
|
|
1203
|
+
/**
|
|
1204
|
+
* Render deterministic Markdown focused on human-readable test intent.
|
|
1205
|
+
*/
|
|
1206
|
+
function formatTestDocumentationOutput(report, context) {
|
|
1207
|
+
const model = (0, test_evidence_core_1.buildSpecificationModel)(report);
|
|
1208
|
+
return `${(0, test_evidence_renderer_md_1.renderTestDocumentationMarkdown)(model, {
|
|
1209
|
+
definitionLinks: resolveDefinitionLinkOptions(context)
|
|
1210
|
+
})}\n`;
|
|
1211
|
+
}
|
|
1212
|
+
function writeTestDocumentationArtifact(args) {
|
|
1213
|
+
(0, node_fs_1.mkdirSync)(node_path_1.default.dirname(args.outPath), { recursive: true });
|
|
1214
|
+
(0, node_fs_1.writeFileSync)(args.outPath, formatTestDocumentationOutput(args.report, {
|
|
1215
|
+
markdownPath: args.outPath,
|
|
1216
|
+
sourceRootDir: args.sourceRootDir
|
|
1217
|
+
}), 'utf8');
|
|
1218
|
+
console.log(`wrote: ${args.outPath}`);
|
|
1219
|
+
}
|
|
1220
|
+
//# sourceMappingURL=testEvidence.js.map
|