@pikku/cli 0.12.70 → 0.12.71
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/cli.schema.json +1 -1
- package/console-app/assets/{index-BY60UXB6.js → index-Bn7IPpsQ.js} +140 -141
- package/console-app/index.html +1 -1
- package/dist/.pikku/agent/pikku-agent-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.d.ts +1 -1
- package/dist/.pikku/channel/pikku-channel-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-channel.js +6 -11
- package/dist/.pikku/cli/pikku-cli-client.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-client.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-contracts-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-types.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings-meta.gen.json +11 -31
- package/dist/.pikku/cli/pikku-cli-wirings.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli-wirings.gen.js +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.d.ts +1 -1
- package/dist/.pikku/cli/pikku-cli.gen.js +1 -1
- package/dist/.pikku/console/pikku-node-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.d.ts +1 -1
- package/dist/.pikku/function/pikku-function-types.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.js +1 -1
- package/dist/.pikku/function/pikku-functions-meta.gen.json +42 -37
- package/dist/.pikku/function/pikku-functions.gen.js +3 -1
- package/dist/.pikku/http/pikku-http-types.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-types.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings-meta.gen.js +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.d.ts +1 -1
- package/dist/.pikku/http/pikku-http-wirings.gen.js +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.d.ts +1 -1
- package/dist/.pikku/mcp/pikku-mcp-types.gen.js +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.d.ts +1 -1
- package/dist/.pikku/pikku-bootstrap.gen.js +1 -1
- package/dist/.pikku/pikku-meta-service.gen.d.ts +1 -1
- package/dist/.pikku/pikku-meta-service.gen.js +1 -1
- package/dist/.pikku/pikku-services.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.d.ts +1 -1
- package/dist/.pikku/pikku-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-types.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings-meta.gen.js +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.d.ts +1 -1
- package/dist/.pikku/queue/pikku-queue-workers-wirings.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.js +1 -1
- package/dist/.pikku/rpc/pikku-rpc-wirings-meta.internal.gen.json +2 -2
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.d.ts +1 -1
- package/dist/.pikku/scheduler/pikku-scheduler-types.gen.js +1 -1
- package/dist/.pikku/schemas/register.gen.js +3 -5
- package/dist/.pikku/schemas/schemas/PikkuCLIConfig.schema.json +1 -1
- package/dist/.pikku/schemas/schemas/PikkuScenarioFunctionsOutput.schema.json +1 -0
- package/dist/.pikku/secrets/pikku-secret-types.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secret-types.gen.js +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.d.ts +1 -1
- package/dist/.pikku/secrets/pikku-secrets.gen.js +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.d.ts +1 -1
- package/dist/.pikku/trigger/pikku-trigger-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variable-types.gen.js +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.d.ts +1 -1
- package/dist/.pikku/variables/pikku-variables.gen.js +1 -1
- package/dist/.pikku/workflow/meta/allWorkflow.gen.json +8 -2
- package/dist/.pikku/workflow/pikku-workflow-types.gen.d.ts +1 -1
- package/dist/.pikku/workflow/pikku-workflow-types.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings-meta.gen.js +1 -1
- package/dist/.pikku/workflow/pikku-workflow-wirings.gen.js +1 -1
- package/dist/bin/pikku-bin.mjs +2 -2
- package/dist/src/cli.wiring.js +11 -29
- package/dist/src/fabric/fabric-commands.d.ts +3 -3
- package/dist/src/fabric/functions/llm-key.function.d.ts +4 -4
- package/dist/src/fabric/functions/validate.function.js +54 -15
- package/dist/src/functions/commands/enable.d.ts +1 -0
- package/dist/src/functions/commands/enable.js +9 -1
- package/dist/src/functions/commands/scenario.js +4 -4
- package/dist/src/functions/commands/start-coverage.d.ts +1 -1
- package/dist/src/functions/commands/start-coverage.js +21 -5
- package/dist/src/functions/validate/workspace-validate.js +0 -4
- package/dist/src/functions/wirings/console/serialize-console-functions.js +0 -6
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.d.ts +1 -0
- package/dist/src/functions/wirings/scenarios/pikku-command-scenario-functions.js +25 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.d.ts +6 -0
- package/dist/src/functions/wirings/scenarios/serialize-scenario-functions.js +98 -0
- package/dist/src/functions/wirings/workflow/pikku-command-workflow.js +4 -5
- package/dist/src/functions/workflows/all.workflow.js +6 -0
- package/dist/src/utils/pikku-cli-config.js +3 -0
- package/dist/src/utils/precise-coverage-map.d.ts +4 -0
- package/dist/src/utils/precise-coverage-map.js +176 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +7 -3
- package/dist/.pikku/schemas/schemas/PikkuTestsCoverageInput.schema.json +0 -1
- package/dist/.pikku/schemas/schemas/PikkuTestsInitInput.schema.json +0 -1
- package/dist/src/functions/commands/tests-coverage.d.ts +0 -10
- package/dist/src/functions/commands/tests-coverage.js +0 -273
- package/dist/src/functions/commands/tests-init.d.ts +0 -7
- package/dist/src/functions/commands/tests-init.js +0 -274
|
@@ -1,273 +0,0 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
-
import { join, relative, dirname } from 'node:path';
|
|
3
|
-
import { spawnSync } from 'node:child_process';
|
|
4
|
-
import ts from 'typescript';
|
|
5
|
-
import { pikkuSessionlessFunc } from '#pikku';
|
|
6
|
-
function innerBody(init) {
|
|
7
|
-
if (!ts.isCallExpression(init))
|
|
8
|
-
return null;
|
|
9
|
-
for (const arg of init.arguments) {
|
|
10
|
-
if (ts.isArrowFunction(arg) || ts.isFunctionExpression(arg))
|
|
11
|
-
return arg.body;
|
|
12
|
-
if (ts.isObjectLiteralExpression(arg)) {
|
|
13
|
-
for (const p of arg.properties) {
|
|
14
|
-
if (ts.isPropertyAssignment(p) &&
|
|
15
|
-
ts.isIdentifier(p.name) &&
|
|
16
|
-
p.name.text === 'func' &&
|
|
17
|
-
(ts.isArrowFunction(p.initializer) ||
|
|
18
|
-
ts.isFunctionExpression(p.initializer)))
|
|
19
|
-
return p.initializer.body;
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
return null;
|
|
24
|
-
}
|
|
25
|
-
function bodySpan(file, exportedName, astCache) {
|
|
26
|
-
let sf = astCache.get(file);
|
|
27
|
-
if (!sf) {
|
|
28
|
-
sf = ts.createSourceFile(file, readFileSync(file, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
29
|
-
astCache.set(file, sf);
|
|
30
|
-
}
|
|
31
|
-
const lineOf = (pos) => sf.getLineAndCharacterOfPosition(pos).line + 1;
|
|
32
|
-
const isExported = (node) => ts.canHaveModifiers(node) &&
|
|
33
|
-
ts.getModifiers(node)?.some((m) => m.kind === ts.SyntaxKind.ExportKeyword);
|
|
34
|
-
const spanOfBody = (body) => {
|
|
35
|
-
if (ts.isBlock(body) && body.statements.length) {
|
|
36
|
-
return {
|
|
37
|
-
start: lineOf(body.statements[0].getStart(sf)),
|
|
38
|
-
end: lineOf(body.statements[body.statements.length - 1].getEnd()),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
return { start: lineOf(body.getStart(sf)), end: lineOf(body.getEnd()) };
|
|
42
|
-
};
|
|
43
|
-
let span = null;
|
|
44
|
-
sf.forEachChild((node) => {
|
|
45
|
-
if (span)
|
|
46
|
-
return;
|
|
47
|
-
if (ts.isVariableStatement(node) && isExported(node)) {
|
|
48
|
-
for (const decl of node.declarationList.declarations) {
|
|
49
|
-
if (ts.isIdentifier(decl.name) &&
|
|
50
|
-
decl.name.text === exportedName &&
|
|
51
|
-
decl.initializer) {
|
|
52
|
-
const body = innerBody(decl.initializer);
|
|
53
|
-
span = body
|
|
54
|
-
? spanOfBody(body)
|
|
55
|
-
: {
|
|
56
|
-
start: lineOf(decl.initializer.getStart(sf)),
|
|
57
|
-
end: lineOf(decl.initializer.getEnd()),
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
else if (ts.isFunctionDeclaration(node) &&
|
|
63
|
-
node.name?.text === exportedName &&
|
|
64
|
-
isExported(node) &&
|
|
65
|
-
node.body) {
|
|
66
|
-
span = spanOfBody(node.body);
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
return span;
|
|
70
|
-
}
|
|
71
|
-
function lineCoverage(fileCov, span) {
|
|
72
|
-
const covered = new Set();
|
|
73
|
-
const missed = new Set();
|
|
74
|
-
if (fileCov) {
|
|
75
|
-
for (const id of Object.keys(fileCov.statementMap)) {
|
|
76
|
-
const ln = fileCov.statementMap[id].start.line;
|
|
77
|
-
if (ln >= span.start && ln <= span.end) {
|
|
78
|
-
if (fileCov.s[id] > 0)
|
|
79
|
-
covered.add(ln);
|
|
80
|
-
else
|
|
81
|
-
missed.add(ln);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
for (const ln of covered)
|
|
86
|
-
missed.delete(ln);
|
|
87
|
-
return {
|
|
88
|
-
covered: [...covered].sort((a, b) => a - b),
|
|
89
|
-
missed: [...missed].sort((a, b) => a - b),
|
|
90
|
-
};
|
|
91
|
-
}
|
|
92
|
-
export const pikkuTestsCoverage = pikkuSessionlessFunc({
|
|
93
|
-
func: async ({ logger, config }, input) => {
|
|
94
|
-
const noRun = input?.noRun ?? false;
|
|
95
|
-
const aiOut = input?.aiOut ?? null;
|
|
96
|
-
const packageMappings = config.packageMappings ?? {};
|
|
97
|
-
const srcDirs = config.srcDirectories ?? [];
|
|
98
|
-
const functionsRelDir = Object.keys(packageMappings).find((key) => srcDirs.some((src) => src === key || src.startsWith(key + '/'))) ?? Object.keys(packageMappings)[0];
|
|
99
|
-
if (!functionsRelDir) {
|
|
100
|
-
logger.error('packageMappings must have at least one entry pointing to your functions package');
|
|
101
|
-
process.exit(1);
|
|
102
|
-
}
|
|
103
|
-
const functionsDir = join(config.rootDir, functionsRelDir);
|
|
104
|
-
const ftestDir = join(functionsDir, 'tests');
|
|
105
|
-
if (!existsSync(ftestDir)) {
|
|
106
|
-
logger.error(`tests directory not found at ${ftestDir}. Run 'pikku tests init' first.`);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
const verboseMetaPath = join(config.outDir, 'function', 'pikku-functions-meta-verbose.gen.json');
|
|
110
|
-
if (!existsSync(verboseMetaPath)) {
|
|
111
|
-
logger.error(`Verbose metadata not found at ${verboseMetaPath}. Run 'pikku all' first.`);
|
|
112
|
-
process.exit(1);
|
|
113
|
-
}
|
|
114
|
-
const coverageFinal = join(functionsDir, '.coverage', 'coverage-final.json');
|
|
115
|
-
const outDir = join(ftestDir, '.coverage');
|
|
116
|
-
const outFile = join(outDir, 'function-coverage.json');
|
|
117
|
-
if (!noRun) {
|
|
118
|
-
const findBin = (name, searchFrom) => {
|
|
119
|
-
let dir = searchFrom;
|
|
120
|
-
for (let i = 0; i < 6; i++) {
|
|
121
|
-
const candidate = join(dir, 'node_modules', '.bin', name);
|
|
122
|
-
if (existsSync(candidate))
|
|
123
|
-
return candidate;
|
|
124
|
-
const parent = dirname(dir);
|
|
125
|
-
if (parent === dir)
|
|
126
|
-
break;
|
|
127
|
-
dir = parent;
|
|
128
|
-
}
|
|
129
|
-
return name; // fallback: assume on PATH
|
|
130
|
-
};
|
|
131
|
-
const c8 = findBin('c8', ftestDir);
|
|
132
|
-
const cucumber = findBin('cucumber-js', ftestDir);
|
|
133
|
-
const envFile = join(ftestDir, '.env.test');
|
|
134
|
-
const spawnEnv = { ...process.env };
|
|
135
|
-
if (existsSync(envFile)) {
|
|
136
|
-
for (const line of readFileSync(envFile, 'utf8').split('\n')) {
|
|
137
|
-
const trimmed = line.trim();
|
|
138
|
-
if (!trimmed || trimmed.startsWith('#'))
|
|
139
|
-
continue;
|
|
140
|
-
const eq = trimmed.indexOf('=');
|
|
141
|
-
if (eq < 0)
|
|
142
|
-
continue;
|
|
143
|
-
spawnEnv[trimmed.slice(0, eq).trim()] = trimmed
|
|
144
|
-
.slice(eq + 1)
|
|
145
|
-
.trim()
|
|
146
|
-
.replace(/^['"]|['"]$/g, '');
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
logger.info('Running function tests under c8...');
|
|
150
|
-
const res = spawnSync(process.execPath, [
|
|
151
|
-
c8,
|
|
152
|
-
'--src',
|
|
153
|
-
'src',
|
|
154
|
-
'--include',
|
|
155
|
-
'src/**',
|
|
156
|
-
'--report-dir',
|
|
157
|
-
'.coverage',
|
|
158
|
-
'--reporter',
|
|
159
|
-
'json',
|
|
160
|
-
'--reporter',
|
|
161
|
-
'text-summary',
|
|
162
|
-
cucumber,
|
|
163
|
-
'--require-module',
|
|
164
|
-
'tsx',
|
|
165
|
-
'--require',
|
|
166
|
-
'tests/tests/support/**/*.ts',
|
|
167
|
-
'tests/tests/features/**/*.feature',
|
|
168
|
-
'--format',
|
|
169
|
-
'progress',
|
|
170
|
-
// Persist the HTML report so the console can parse scenarios back out
|
|
171
|
-
// of it (readAllMeta attaches them per function).
|
|
172
|
-
'--format',
|
|
173
|
-
'html:tests/tests/reports/cucumber-report.html',
|
|
174
|
-
], { cwd: functionsDir, stdio: 'inherit', env: spawnEnv });
|
|
175
|
-
if (res.status !== 0) {
|
|
176
|
-
logger.error(`Test run failed (exit ${res.status})`);
|
|
177
|
-
process.exit(res.status ?? 1);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
if (!existsSync(coverageFinal)) {
|
|
181
|
-
logger.error(`coverage-final.json not found at ${coverageFinal}. Run without --no-run.`);
|
|
182
|
-
process.exit(1);
|
|
183
|
-
}
|
|
184
|
-
const coverage = JSON.parse(readFileSync(coverageFinal, 'utf8'));
|
|
185
|
-
const meta = JSON.parse(readFileSync(verboseMetaPath, 'utf8'));
|
|
186
|
-
const astCache = new Map();
|
|
187
|
-
const functions = Object.values(meta)
|
|
188
|
-
.filter((m) => m.sourceFile && !m.sourceFile.endsWith('.gen.ts'))
|
|
189
|
-
.map((m) => {
|
|
190
|
-
const span = bodySpan(m.sourceFile, m.exportedName, astCache);
|
|
191
|
-
const { covered, missed } = span
|
|
192
|
-
? lineCoverage(coverage[m.sourceFile], span)
|
|
193
|
-
: { covered: [], missed: [] };
|
|
194
|
-
const total = covered.length + missed.length;
|
|
195
|
-
const ratio = total ? covered.length / total : 0;
|
|
196
|
-
const status = total === 0
|
|
197
|
-
? 'unknown'
|
|
198
|
-
: covered.length === 0
|
|
199
|
-
? 'uncovered'
|
|
200
|
-
: missed.length === 0
|
|
201
|
-
? 'covered'
|
|
202
|
-
: 'partial';
|
|
203
|
-
return {
|
|
204
|
-
name: m.name,
|
|
205
|
-
sourceFile: relative(functionsDir, m.sourceFile),
|
|
206
|
-
exposed: m.expose !== false,
|
|
207
|
-
description: m.description ?? null,
|
|
208
|
-
coveredLines: covered.length,
|
|
209
|
-
totalLines: total,
|
|
210
|
-
missedLines: missed,
|
|
211
|
-
ratio: Number(ratio.toFixed(3)),
|
|
212
|
-
status,
|
|
213
|
-
};
|
|
214
|
-
})
|
|
215
|
-
.sort((a, b) => a.ratio - b.ratio || a.name.localeCompare(b.name));
|
|
216
|
-
const summary = {
|
|
217
|
-
total: functions.length,
|
|
218
|
-
covered: 0,
|
|
219
|
-
partial: 0,
|
|
220
|
-
uncovered: 0,
|
|
221
|
-
unknown: 0,
|
|
222
|
-
};
|
|
223
|
-
for (const f of functions)
|
|
224
|
-
summary[f.status]++;
|
|
225
|
-
const overallRatio = functions.length
|
|
226
|
-
? Number((functions.reduce((a, f) => a + f.ratio, 0) / functions.length).toFixed(3))
|
|
227
|
-
: 0;
|
|
228
|
-
mkdirSync(outDir, { recursive: true });
|
|
229
|
-
writeFileSync(outFile, JSON.stringify({
|
|
230
|
-
generatedAt: new Date().toISOString(),
|
|
231
|
-
summary: { ...summary, overallRatio },
|
|
232
|
-
functions,
|
|
233
|
-
}, null, 2) + '\n');
|
|
234
|
-
const uncovered = functions.filter((f) => f.status === 'uncovered');
|
|
235
|
-
console.log(`\nFunction coverage → ${relative(config.rootDir, outFile)}`);
|
|
236
|
-
console.log(` ${summary.covered} covered · ${summary.partial} partial · ${summary.uncovered} uncovered · ${summary.unknown} unknown (overall ${(overallRatio * 100).toFixed(1)}%)`);
|
|
237
|
-
if (uncovered.length)
|
|
238
|
-
console.log(` uncovered: ${uncovered.map((f) => f.name).join(', ')}`);
|
|
239
|
-
if (aiOut !== null) {
|
|
240
|
-
const generatedAt = new Date().toISOString();
|
|
241
|
-
const pct = Math.round(overallRatio * 100);
|
|
242
|
-
const needWork = functions.filter((f) => f.status !== 'covered');
|
|
243
|
-
const lines = [
|
|
244
|
-
`Coverage report — generated ${generatedAt}.`,
|
|
245
|
-
`Overall: ${pct}% (${summary.covered}/${summary.total} functions fully covered)`,
|
|
246
|
-
'',
|
|
247
|
-
needWork.length === 0
|
|
248
|
-
? 'All functions are fully covered — nothing to do!'
|
|
249
|
-
: `Functions needing coverage (${needWork.length}):`,
|
|
250
|
-
...needWork.flatMap((fn) => {
|
|
251
|
-
const ratio = Math.round(fn.ratio * 100);
|
|
252
|
-
const missed = fn.missedLines.length > 0 ? fn.missedLines.join(', ') : 'none';
|
|
253
|
-
return [
|
|
254
|
-
`- ${fn.name} [${fn.status}, ${ratio}% covered, ${fn.coveredLines}/${fn.totalLines} lines]`,
|
|
255
|
-
` file: ${fn.sourceFile}`,
|
|
256
|
-
` missed lines: ${missed}`,
|
|
257
|
-
];
|
|
258
|
-
}),
|
|
259
|
-
];
|
|
260
|
-
if (needWork.length > 0) {
|
|
261
|
-
lines.push('', 'Write @pikku/cucumber Gherkin scenarios (no custom steps) in tests/tests/features/ to cover the missed lines above.', 'Use pikku meta to get versioned RPC names and function schemas before writing.', 'Run pikku tests coverage after writing to verify coverage improved.');
|
|
262
|
-
}
|
|
263
|
-
const prompt = lines.join('\n') + '\n';
|
|
264
|
-
if (aiOut === '-') {
|
|
265
|
-
process.stdout.write(prompt);
|
|
266
|
-
}
|
|
267
|
-
else {
|
|
268
|
-
writeFileSync(aiOut, prompt, 'utf-8');
|
|
269
|
-
console.log(`AI prompt → ${relative(config.rootDir, aiOut)}`);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
},
|
|
273
|
-
});
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare const pikkuTestsInit: import("#pikku").PikkuFunctionConfig<{
|
|
2
|
-
force?: boolean;
|
|
3
|
-
}, void, "rpc" | "session", import("#pikku").PikkuFunctionSessionless<{
|
|
4
|
-
force?: boolean;
|
|
5
|
-
}, void, "rpc" | "session", import("#pikku").Services> | import("#pikku").PikkuFunction<{
|
|
6
|
-
force?: boolean;
|
|
7
|
-
}, void, "rpc" | "session", import("#pikku").Services>, undefined, undefined>;
|
|
@@ -1,274 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'fs';
|
|
2
|
-
import { mkdir, writeFile } from 'fs/promises';
|
|
3
|
-
import { dirname, join, relative } from 'path';
|
|
4
|
-
import { pikkuSessionlessFunc } from '#pikku';
|
|
5
|
-
const toJs = (p) => p.replace(/\.(?:d\.)?ts$/, '.js');
|
|
6
|
-
// ---------------------------------------------------------------------------
|
|
7
|
-
// Template generators
|
|
8
|
-
// ---------------------------------------------------------------------------
|
|
9
|
-
function packageJson() {
|
|
10
|
-
return JSON.stringify({
|
|
11
|
-
name: 'tests',
|
|
12
|
-
version: '0.0.1',
|
|
13
|
-
private: true,
|
|
14
|
-
type: 'module',
|
|
15
|
-
description: 'Function test harness — Cucumber over in-process Pikku RPC with stubbed services.',
|
|
16
|
-
scripts: {
|
|
17
|
-
test: "node --env-file=.env.test node_modules/.bin/cucumber-js --config tests/cucumber.mjs --tags 'not @skip'",
|
|
18
|
-
'test:tag': 'node --env-file=.env.test node_modules/.bin/cucumber-js --config tests/cucumber.mjs --tags',
|
|
19
|
-
coverage: 'pikku tests coverage',
|
|
20
|
-
tsc: 'tsc --noEmit',
|
|
21
|
-
},
|
|
22
|
-
dependencies: {
|
|
23
|
-
'@pikku/cucumber': '^0.12.0',
|
|
24
|
-
'@pikku/kysely-node-sqlite': '^0.12.0',
|
|
25
|
-
},
|
|
26
|
-
devDependencies: {
|
|
27
|
-
'@cucumber/cucumber': '^11.0.0',
|
|
28
|
-
'@types/node': '^22',
|
|
29
|
-
c8: '^10.0.0',
|
|
30
|
-
tsx: '^4.21.0',
|
|
31
|
-
typescript: '~5.8.0',
|
|
32
|
-
},
|
|
33
|
-
}, null, 2);
|
|
34
|
-
}
|
|
35
|
-
function tsconfig() {
|
|
36
|
-
return JSON.stringify({
|
|
37
|
-
compilerOptions: {
|
|
38
|
-
target: 'ES2022',
|
|
39
|
-
module: 'NodeNext',
|
|
40
|
-
moduleResolution: 'NodeNext',
|
|
41
|
-
types: ['node'],
|
|
42
|
-
noEmit: true,
|
|
43
|
-
skipLibCheck: true,
|
|
44
|
-
esModuleInterop: true,
|
|
45
|
-
resolveJsonModule: true,
|
|
46
|
-
strict: true,
|
|
47
|
-
},
|
|
48
|
-
include: ['tests/**/*.ts'],
|
|
49
|
-
}, null, 2);
|
|
50
|
-
}
|
|
51
|
-
function envTest() {
|
|
52
|
-
return `# Environment variables required by createConfig() during tests.
|
|
53
|
-
# Add any env vars your config.ts reads at module load time.
|
|
54
|
-
# This file is loaded via --env-file and is NOT committed to version control.
|
|
55
|
-
NODE_ENV=test
|
|
56
|
-
`;
|
|
57
|
-
}
|
|
58
|
-
function cucumberMjs() {
|
|
59
|
-
return `export default {
|
|
60
|
-
requireModule: ['tsx'],
|
|
61
|
-
require: ['tests/support/**/*.ts', 'tests/steps/**/*.ts'],
|
|
62
|
-
paths: ['tests/features/**/*.feature'],
|
|
63
|
-
format: ['progress', 'html:tests/reports/cucumber-report.html'],
|
|
64
|
-
forceExit: true,
|
|
65
|
-
}
|
|
66
|
-
`;
|
|
67
|
-
}
|
|
68
|
-
function hooksTs(bootstrapImport) {
|
|
69
|
-
return `import '${bootstrapImport}'
|
|
70
|
-
import { Before, After, BeforeAll, AfterAll, setDefaultTimeout, Given, When, Then } from '@cucumber/cucumber'
|
|
71
|
-
import { registerHooks, registerCommonSteps } from '@pikku/cucumber'
|
|
72
|
-
import { db } from './services.js'
|
|
73
|
-
|
|
74
|
-
registerHooks({ Before, After, BeforeAll, AfterAll, setDefaultTimeout }, db)
|
|
75
|
-
registerCommonSteps({ Given, When, Then })
|
|
76
|
-
`;
|
|
77
|
-
}
|
|
78
|
-
function starterFeature() {
|
|
79
|
-
return `Feature: Example function test
|
|
80
|
-
|
|
81
|
-
Starter scenario created by \`pikku tests init\`. It uses only the built-in
|
|
82
|
-
pikku/cucumber steps (no custom step code) and passes out of the box, so the
|
|
83
|
-
Run-tests button and coverage report work immediately. Replace it with real
|
|
84
|
-
scenarios that call your RPCs — see the commented example at the bottom.
|
|
85
|
-
|
|
86
|
-
Scenario: the function-test harness is wired up
|
|
87
|
-
Given the data "example" is:
|
|
88
|
-
| hello | world |
|
|
89
|
-
Then the data "example" is not empty
|
|
90
|
-
|
|
91
|
-
# Real example — call one of your RPCs and assert the outcome. Uncomment and
|
|
92
|
-
# adapt (run \`pikku meta\` to list versioned RPC names and input schemas):
|
|
93
|
-
#
|
|
94
|
-
# Scenario: an anonymous user cannot reach a protected RPC
|
|
95
|
-
# When an anonymous user calls "yourProtectedRpc"
|
|
96
|
-
# Then the call fails with "Unauthorized"
|
|
97
|
-
#
|
|
98
|
-
# Scenario: a public RPC returns data
|
|
99
|
-
# When an anonymous user calls "yourPublicRpc" with:
|
|
100
|
-
# | someField | someValue |
|
|
101
|
-
# Then the call succeeds
|
|
102
|
-
# And the result has "id"
|
|
103
|
-
`;
|
|
104
|
-
}
|
|
105
|
-
function worldTs() {
|
|
106
|
-
return `import { World, setWorldConstructor } from '@cucumber/cucumber'
|
|
107
|
-
import { createFunctionWorld } from '@pikku/cucumber'
|
|
108
|
-
import { createStubServices } from './services.js'
|
|
109
|
-
|
|
110
|
-
createFunctionWorld(World, setWorldConstructor, createStubServices)
|
|
111
|
-
`;
|
|
112
|
-
}
|
|
113
|
-
function servicesTs(configImport, servicesImport, schemaImport, coercionImport, configVar, servicesVar, repoRootRel, hasDb, migrationsRel, seedRel) {
|
|
114
|
-
if (!hasDb) {
|
|
115
|
-
return `import { createDbUtils, type StubTracker } from '@pikku/cucumber'
|
|
116
|
-
import { ${configVar} } from '${configImport}'
|
|
117
|
-
import { ${servicesVar} } from '${servicesImport}'
|
|
118
|
-
|
|
119
|
-
export const db = createDbUtils({ migrationsDir: '', seedFile: '' })
|
|
120
|
-
|
|
121
|
-
export async function createStubServices(_dbFile: string, tracker: StubTracker) {
|
|
122
|
-
const injected = new Proxy({} as Record<string, unknown>, {
|
|
123
|
-
get(_, prop: string) { return tracker.stub(prop) },
|
|
124
|
-
})
|
|
125
|
-
const config = await ${configVar}()
|
|
126
|
-
const services = await ${servicesVar}(config as never, injected as never)
|
|
127
|
-
return { services }
|
|
128
|
-
}
|
|
129
|
-
`;
|
|
130
|
-
}
|
|
131
|
-
return `import { createNodeSqliteKysely, createCoercionPlugin } from '@pikku/kysely-node-sqlite'
|
|
132
|
-
import { createDbUtils, type StubTracker } from '@pikku/cucumber'
|
|
133
|
-
import { ${configVar} } from '${configImport}'
|
|
134
|
-
import { ${servicesVar} } from '${servicesImport}'
|
|
135
|
-
import { coercionMap } from '${coercionImport}'
|
|
136
|
-
import type { DB } from '${schemaImport}'
|
|
137
|
-
import { dirname, resolve } from 'node:path'
|
|
138
|
-
import { fileURLToPath } from 'node:url'
|
|
139
|
-
|
|
140
|
-
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
141
|
-
const repoRoot = (p: string) => resolve(__dirname, '${repoRootRel}', p)
|
|
142
|
-
|
|
143
|
-
export const db = createDbUtils({
|
|
144
|
-
migrationsDir: repoRoot('${migrationsRel}'),
|
|
145
|
-
seedFile: repoRoot('${seedRel}'),
|
|
146
|
-
})
|
|
147
|
-
|
|
148
|
-
type StubKysely = ReturnType<typeof createNodeSqliteKysely<DB>>
|
|
149
|
-
|
|
150
|
-
const REAL_SERVICES = (kysely: StubKysely) => ({ kysely })
|
|
151
|
-
|
|
152
|
-
export async function createStubServices(dbFile: string, tracker: StubTracker) {
|
|
153
|
-
const kysely = createNodeSqliteKysely<DB>({
|
|
154
|
-
filename: dbFile,
|
|
155
|
-
camelCase: true,
|
|
156
|
-
plugins: [createCoercionPlugin({ map: coercionMap })],
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
const real = REAL_SERVICES(kysely)
|
|
160
|
-
const injected = new Proxy(real as Record<string, unknown>, {
|
|
161
|
-
get(target, prop: string) {
|
|
162
|
-
if (prop in target) return target[prop]
|
|
163
|
-
return tracker.stub(prop)
|
|
164
|
-
},
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
const config = await ${configVar}()
|
|
168
|
-
const services = await ${servicesVar}(
|
|
169
|
-
{ ...config, dev: { db: { file: dbFile } } } as never,
|
|
170
|
-
injected as never,
|
|
171
|
-
)
|
|
172
|
-
|
|
173
|
-
return { services, kysely }
|
|
174
|
-
}
|
|
175
|
-
`;
|
|
176
|
-
}
|
|
177
|
-
// ---------------------------------------------------------------------------
|
|
178
|
-
// Command
|
|
179
|
-
// ---------------------------------------------------------------------------
|
|
180
|
-
export const pikkuTestsInit = pikkuSessionlessFunc({
|
|
181
|
-
func: async ({ logger, config, getInspectorState }, input) => {
|
|
182
|
-
const force = input?.force;
|
|
183
|
-
const state = await getInspectorState(true);
|
|
184
|
-
const { pikkuConfigFactory, singletonServicesFactory } = state.filesAndMethods;
|
|
185
|
-
if (!pikkuConfigFactory) {
|
|
186
|
-
logger.error('createConfig not found — ensure it is exported from your project');
|
|
187
|
-
process.exit(1);
|
|
188
|
-
}
|
|
189
|
-
if (!singletonServicesFactory) {
|
|
190
|
-
logger.error('createSingletonServices not found — ensure it is exported from your project');
|
|
191
|
-
process.exit(1);
|
|
192
|
-
}
|
|
193
|
-
const packageMappings = config.packageMappings ?? {};
|
|
194
|
-
const srcDirs = config.srcDirectories ?? [];
|
|
195
|
-
const functionsRelDir = Object.keys(packageMappings).find((key) => srcDirs.some((src) => src === key || src.startsWith(key + '/'))) ?? Object.keys(packageMappings)[0];
|
|
196
|
-
if (!functionsRelDir) {
|
|
197
|
-
logger.error('packageMappings must have at least one entry pointing to your functions package');
|
|
198
|
-
process.exit(1);
|
|
199
|
-
}
|
|
200
|
-
const functionsDir = join(config.rootDir, functionsRelDir);
|
|
201
|
-
const ftestDir = join(functionsDir, 'tests');
|
|
202
|
-
if (existsSync(ftestDir) && !force) {
|
|
203
|
-
logger.error(`tests directory already exists at ${ftestDir}. Use --force to overwrite.`);
|
|
204
|
-
process.exit(1);
|
|
205
|
-
}
|
|
206
|
-
const supportDir = join(ftestDir, 'tests', 'support');
|
|
207
|
-
const rel = (abs) => {
|
|
208
|
-
const r = relative(supportDir, abs);
|
|
209
|
-
return r.startsWith('.') ? r : './' + r;
|
|
210
|
-
};
|
|
211
|
-
const bootstrapImport = toJs(rel(join(config.outDir, 'pikku-bootstrap.gen.ts')));
|
|
212
|
-
const configImport = toJs(rel(pikkuConfigFactory.file));
|
|
213
|
-
const servicesImport = toJs(rel(singletonServicesFactory.file));
|
|
214
|
-
// coercion/schema may live under outDir/db (new default) or rootDir/db (legacy)
|
|
215
|
-
const schemaInOutDir = join(config.outDir, 'db', 'schema.gen.ts');
|
|
216
|
-
const schemaInRootDir = join(config.rootDir, 'db', 'schema.gen.ts');
|
|
217
|
-
const schemaFile = existsSync(schemaInOutDir)
|
|
218
|
-
? schemaInOutDir
|
|
219
|
-
: schemaInRootDir;
|
|
220
|
-
const coercionInOutDir = join(config.outDir, 'db', 'coercion.gen.ts');
|
|
221
|
-
const coercionInRootDir = join(config.rootDir, 'db', 'coercion.gen.ts');
|
|
222
|
-
const coercionFile = existsSync(coercionInOutDir)
|
|
223
|
-
? coercionInOutDir
|
|
224
|
-
: coercionInRootDir;
|
|
225
|
-
const schemaImport = toJs(rel(schemaFile));
|
|
226
|
-
const coercionImport = toJs(rel(coercionFile));
|
|
227
|
-
const repoRootRel = relative(supportDir, config.rootDir);
|
|
228
|
-
// Engine-aware db layout: stages live under db/sqlite or db/postgres with a
|
|
229
|
-
// matching db/<engine>-seed.sql (see the deploy convention). Pick whichever
|
|
230
|
-
// engine the project actually ships migrations for.
|
|
231
|
-
const engine = existsSync(join(config.rootDir, 'db', 'sqlite'))
|
|
232
|
-
? 'sqlite'
|
|
233
|
-
: existsSync(join(config.rootDir, 'db', 'postgres'))
|
|
234
|
-
? 'postgres'
|
|
235
|
-
: null;
|
|
236
|
-
// Only sqlite runs in the harness today (node:sqlite); postgres falls back
|
|
237
|
-
// to stubbed services until a pglite-backed harness lands.
|
|
238
|
-
const hasDb = engine === 'sqlite';
|
|
239
|
-
const migrationsRel = engine ? `db/${engine}` : '';
|
|
240
|
-
const seedRel = engine ? `db/${engine}-seed.sql` : '';
|
|
241
|
-
if (engine === 'postgres') {
|
|
242
|
-
logger.info('Note: Postgres function-test harness support is not wired yet (the harness runs on node:sqlite). Tracking in pikkujs/pikku#758 — the scaffold falls back to stubbed services.');
|
|
243
|
-
}
|
|
244
|
-
const files = [
|
|
245
|
-
[join(ftestDir, '.env.test'), envTest()],
|
|
246
|
-
// Empty lockfile so Yarn Berry treats tests/ as a standalone project
|
|
247
|
-
// rather than expecting it in the parent repo's workspaces (the harness
|
|
248
|
-
// is intentionally outside the workspace graph).
|
|
249
|
-
[join(ftestDir, 'yarn.lock'), ''],
|
|
250
|
-
[join(ftestDir, 'package.json'), packageJson()],
|
|
251
|
-
[join(ftestDir, 'tsconfig.json'), tsconfig()],
|
|
252
|
-
[join(ftestDir, 'tests', 'cucumber.mjs'), cucumberMjs()],
|
|
253
|
-
[join(supportDir, 'hooks.ts'), hooksTs(bootstrapImport)],
|
|
254
|
-
[join(supportDir, 'world.ts'), worldTs()],
|
|
255
|
-
[
|
|
256
|
-
join(supportDir, 'services.ts'),
|
|
257
|
-
servicesTs(configImport, servicesImport, schemaImport, coercionImport, pikkuConfigFactory.variable, singletonServicesFactory.variable, repoRootRel, hasDb, migrationsRel, seedRel),
|
|
258
|
-
],
|
|
259
|
-
[
|
|
260
|
-
join(ftestDir, 'tests', 'features', 'example.feature'),
|
|
261
|
-
starterFeature(),
|
|
262
|
-
],
|
|
263
|
-
];
|
|
264
|
-
for (const [filePath, content] of files) {
|
|
265
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
266
|
-
await writeFile(filePath, content, 'utf-8');
|
|
267
|
-
logger.info(` created ${relative(config.rootDir, filePath)}`);
|
|
268
|
-
}
|
|
269
|
-
logger.info('\nFunction test harness initialized.');
|
|
270
|
-
logger.info('Next steps:');
|
|
271
|
-
logger.info(' 1. Install deps in tests/ and run: yarn test');
|
|
272
|
-
logger.info(' 2. Edit tests/tests/features/example.feature — add scenarios that call your RPCs');
|
|
273
|
-
},
|
|
274
|
-
});
|