@pikku/core 0.12.136 → 0.12.138
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/CHANGELOG.md +41 -0
- package/dist/dev/hot-reload.js +5 -2
- package/dist/env.d.ts +18 -0
- package/dist/env.js +60 -1
- package/dist/function/abort-scope.d.ts +6 -5
- package/dist/function/abort-scope.js +6 -17
- package/dist/function/function-runner.js +2 -1
- package/dist/middleware/auth-apikey.js +1 -1
- package/dist/middleware/auth-bearer.js +1 -1
- package/dist/middleware/auth-cookie.js +1 -1
- package/dist/schema.js +3 -1
- package/dist/services/in-memory-queue-service.js +4 -2
- package/dist/services/in-memory-workflow-service.js +7 -6
- package/dist/services/index.d.ts +0 -1
- package/dist/services/index.js +0 -1
- package/dist/services/local-variables.d.ts +5 -0
- package/dist/services/local-variables.js +7 -2
- package/dist/services/meta-service.d.ts +11 -0
- package/dist/services/meta-service.js +10 -1
- package/dist/services/queue-webhook-service.js +1 -1
- package/dist/services/webhook-service.d.ts +2 -2
- package/dist/services/webhook-service.js +6 -4
- package/dist/types/core.types.d.ts +10 -1
- package/dist/utils/hmac.d.ts +25 -10
- package/dist/utils/hmac.js +202 -24
- package/dist/wirings/agent/agent-agui.js +65 -62
- package/dist/wirings/agent/agent-interrupt.d.ts +4 -1
- package/dist/wirings/agent/agent-interrupt.js +3 -3
- package/dist/wirings/agent/agent-memory.js +5 -6
- package/dist/wirings/agent/agent-model-config.js +2 -1
- package/dist/wirings/agent/agent-prepare.js +21 -13
- package/dist/wirings/agent/agent-stream.js +38 -34
- package/dist/wirings/agent/agent.types.d.ts +8 -1
- package/dist/wirings/channel/channel-handler.js +1 -1
- package/dist/wirings/channel/local/local-channel-handler.js +5 -1
- package/dist/wirings/channel/local/local-channel-runner.js +6 -4
- package/dist/wirings/channel/local/local-eventhub-service.d.ts +1 -0
- package/dist/wirings/channel/local/local-eventhub-service.js +4 -1
- package/dist/wirings/cli/channel/cli-channel-runner.js +7 -2
- package/dist/wirings/cli/channel/cli-raw-client-runner.js +8 -4
- package/dist/wirings/cli/cli-runner.js +1 -1
- package/dist/wirings/gateway/gateway-runner.js +3 -3
- package/dist/wirings/http/http-runner.js +6 -6
- package/dist/wirings/persona/persona-app-scopes.js +1 -0
- package/dist/wirings/scheduler/scheduler-runner.js +1 -1
- package/dist/wirings/scope/define-scope.d.ts +19 -0
- package/dist/wirings/scope/define-scope.js +19 -0
- package/dist/wirings/scope/index.d.ts +2 -2
- package/dist/wirings/scope/index.js +1 -1
- package/dist/wirings/scope/scope.types.d.ts +14 -0
- package/dist/wirings/scope/validate-scope-definitions.js +1 -0
- package/dist/wirings/trigger/trigger-runner.js +3 -1
- package/dist/wirings/trigger/webhook-source-runner.js +5 -2
- package/dist/wirings/virtual-user/virtual-user-scaffold.js +3 -2
- package/dist/wirings/workflow/scenario-coverage.d.ts +70 -0
- package/dist/wirings/workflow/scenario-coverage.js +159 -0
- package/dist/wirings/workflow/workflow-invocation-id.js +85 -8
- package/package.json +4 -1
- package/src/public-surface.json +10 -3
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export { defineScope } from './define-scope.js';
|
|
1
|
+
export { defineScope, declareScopes } from './define-scope.js';
|
|
2
2
|
export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
|
|
3
|
-
export type { CoreScopes, CoreScopeNode, FlatScope, ScopeDefinitionMeta, ScopeDefinitionsMeta, ScopeDefinitions, ScopeNodeMeta, } from './scope.types.js';
|
|
3
|
+
export type { CoreScopes, CoreScopeNode, FlatScope, ScopeDefinitionMeta, ScopeDefinitionsMeta, ScopeDefinitions, ScopeNodeMeta, ScopeOrigin, } from './scope.types.js';
|
|
4
4
|
export { hasScopes, verifyScopes } from '../../scopes.js';
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { defineScope } from './define-scope.js';
|
|
1
|
+
export { defineScope, declareScopes } from './define-scope.js';
|
|
2
2
|
export { flattenScopeDefinitions, validateAndBuildScopeDefinitionsMeta, } from './validate-scope-definitions.js';
|
|
3
3
|
export { hasScopes, verifyScopes } from '../../scopes.js';
|
|
@@ -21,12 +21,26 @@ export type ScopeNodeMeta = {
|
|
|
21
21
|
description?: string;
|
|
22
22
|
scopes?: Record<string, ScopeNodeMeta>;
|
|
23
23
|
};
|
|
24
|
+
/**
|
|
25
|
+
* Who put a scope tree into the app: the app's own `defineScope`, a file the
|
|
26
|
+
* CLI generated into the project, or an installed addon.
|
|
27
|
+
*/
|
|
28
|
+
export type ScopeOrigin = {
|
|
29
|
+
kind: 'app';
|
|
30
|
+
} | {
|
|
31
|
+
kind: 'generated';
|
|
32
|
+
} | {
|
|
33
|
+
kind: 'addon';
|
|
34
|
+
package: string;
|
|
35
|
+
displayName?: string;
|
|
36
|
+
};
|
|
24
37
|
export type ScopeDefinitionMeta = {
|
|
25
38
|
name: string;
|
|
26
39
|
displayName?: string;
|
|
27
40
|
description?: string;
|
|
28
41
|
scopes?: Record<string, ScopeNodeMeta>;
|
|
29
42
|
sourceFile?: string;
|
|
43
|
+
origin?: ScopeOrigin;
|
|
30
44
|
};
|
|
31
45
|
export type ScopeDefinitions = ScopeDefinitionMeta[];
|
|
32
46
|
export type ScopeDefinitionsMeta = Record<string, ScopeDefinitionMeta>;
|
|
@@ -43,7 +43,9 @@ export async function setupTrigger({ name, input, onTrigger, }) {
|
|
|
43
43
|
trigger: {
|
|
44
44
|
invoke: (data) => {
|
|
45
45
|
singletonServices.logger.info(`Trigger fired: ${name}`);
|
|
46
|
-
|
|
46
|
+
// `invoke` is typed void for trigger sources, so a rejection from the
|
|
47
|
+
// trigger function is logged here rather than left unhandled.
|
|
48
|
+
Promise.resolve(onTrigger(data)).catch((e) => singletonServices.logger.error(`Trigger '${name}' handler failed: ${e?.message ?? e}`));
|
|
47
49
|
},
|
|
48
50
|
},
|
|
49
51
|
};
|
|
@@ -63,6 +63,9 @@ const readRequest = async (http) => {
|
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
65
|
const signedWith = async (verify, request, secret, services) => {
|
|
66
|
+
// Fail closed: an empty secret never verifies, whatever the scheme.
|
|
67
|
+
if (!secret)
|
|
68
|
+
return false;
|
|
66
69
|
if (typeof verify === 'function') {
|
|
67
70
|
return await verify(request, secret, services);
|
|
68
71
|
}
|
|
@@ -72,14 +75,14 @@ const signedWith = async (verify, request, secret, services) => {
|
|
|
72
75
|
};
|
|
73
76
|
if ('hmac' in verify) {
|
|
74
77
|
const { header: name, prefix, algorithm, encoding, secretEncoding, } = verify.hmac;
|
|
75
|
-
return verifyHmacSignature(secret, header(name, prefix), algorithm, request.body, encoding, secretEncoding);
|
|
78
|
+
return await verifyHmacSignature(secret, header(name, prefix), algorithm, request.body, encoding, secretEncoding);
|
|
76
79
|
}
|
|
77
80
|
if ('token' in verify) {
|
|
78
81
|
const token = header(verify.token.header, verify.token.prefix);
|
|
79
82
|
return !!token && timingSafeStringEqual(token, secret);
|
|
80
83
|
}
|
|
81
84
|
const { header: name, ...options } = verify.publicKey;
|
|
82
|
-
return verifyPublicKeySignature(secret, header(name), request.body, options);
|
|
85
|
+
return await verifyPublicKeySignature(secret, header(name), request.body, options);
|
|
83
86
|
};
|
|
84
87
|
/**
|
|
85
88
|
* Whether the request was signed with the source's secret. A request with a
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { readEnvVariable } from '../../env.js';
|
|
1
2
|
import { BadRequestError, ForbiddenError } from '../../errors/errors.js';
|
|
2
3
|
import { prepareVirtualUserRun } from './prepare-virtual-user-run.js';
|
|
3
4
|
import { runVirtualUser as runVirtualUserEngine } from './run-virtual-user.js';
|
|
@@ -122,7 +123,7 @@ const refuseInProduction = (personaId, disposition, config, environments, enviro
|
|
|
122
123
|
* scheduled tick have in common. The dispatch that follows is typed off the
|
|
123
124
|
* app's RPC map, so it stays in the generated wiring.
|
|
124
125
|
*/
|
|
125
|
-
export const startVirtualUserRun = async ({ store, personas, config, environments, environment =
|
|
126
|
+
export const startVirtualUserRun = async ({ store, personas, config, environments, environment = readEnvVariable('PIKKU_ENV'), persona: personaId, disposition: requested, seed: requestedSeed, goals, memory, startedBy, }) => {
|
|
126
127
|
const runStore = requireVirtualUserRunStore(store);
|
|
127
128
|
const persona = runnablePersona(personas, personaId);
|
|
128
129
|
const disposition = (requested ??
|
|
@@ -251,7 +252,7 @@ export const serializeVirtualUserSchedule = (schedule, personas) => {
|
|
|
251
252
|
* keeps what it had: enabling a row saved earlier as adversarial is refused
|
|
252
253
|
* just as saving one would be.
|
|
253
254
|
*/
|
|
254
|
-
export const writeVirtualUserSchedule = async ({ store, personas, persona, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, config, environments, environment =
|
|
255
|
+
export const writeVirtualUserSchedule = async ({ store, personas, persona, enabled, disposition, goals, budget, minIntervalMs, maxIntervalMs, nextRunAt, config, environments, environment = readEnvVariable('PIKKU_ENV'), }) => {
|
|
255
256
|
const scheduleStore = requireVirtualUserScheduleStore(store);
|
|
256
257
|
const declared = runnablePersona(personas, persona);
|
|
257
258
|
const current = enabled === undefined || disposition === undefined
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { MetaService } from '../../services/meta-service.js';
|
|
2
|
+
/** Per-function line coverage as `pikku scenario run --coverage` records it. */
|
|
3
|
+
export interface ScenarioFunctionCoverage {
|
|
4
|
+
name: string;
|
|
5
|
+
sourceFile: string;
|
|
6
|
+
status: 'covered' | 'partial' | 'uncovered' | 'unknown';
|
|
7
|
+
totalLines: number;
|
|
8
|
+
missedLines: number[];
|
|
9
|
+
}
|
|
10
|
+
/** The file `pikku scenario run --coverage` writes to `.pikku/coverage/scenario-coverage.json`. */
|
|
11
|
+
export interface ScenarioCoverageFile {
|
|
12
|
+
generatedAt: string;
|
|
13
|
+
environment: string;
|
|
14
|
+
scenarios: Record<string, {
|
|
15
|
+
functions?: ScenarioFunctionCoverage[];
|
|
16
|
+
summary?: {
|
|
17
|
+
total?: number;
|
|
18
|
+
};
|
|
19
|
+
}>;
|
|
20
|
+
}
|
|
21
|
+
/** A function with lines no scenario reaches. */
|
|
22
|
+
export interface ScenarioCoverageGap {
|
|
23
|
+
function: string;
|
|
24
|
+
sourceFile: string;
|
|
25
|
+
status: 'uncovered' | 'partial';
|
|
26
|
+
/** Collapsed line ranges, e.g. `["L14-31", "L42"]`. */
|
|
27
|
+
missing: string[];
|
|
28
|
+
missedLines: number;
|
|
29
|
+
totalLines: number;
|
|
30
|
+
}
|
|
31
|
+
/** A mutation a user can perform that no scenario drives. */
|
|
32
|
+
export interface UncoveredMutation {
|
|
33
|
+
id: string;
|
|
34
|
+
sourceFile?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface ScenarioCoverage {
|
|
37
|
+
/** Line coverage across every scenario; `null` until a run with `--coverage` has been recorded. */
|
|
38
|
+
api: {
|
|
39
|
+
generatedAt: string;
|
|
40
|
+
environment: string;
|
|
41
|
+
pct: number;
|
|
42
|
+
covered: number;
|
|
43
|
+
total: number;
|
|
44
|
+
gaps: ScenarioCoverageGap[];
|
|
45
|
+
} | null;
|
|
46
|
+
mutations: {
|
|
47
|
+
required: number;
|
|
48
|
+
covered: number;
|
|
49
|
+
uncovered: UncoveredMutation[];
|
|
50
|
+
};
|
|
51
|
+
/** Paths a scenario opens; `unvisited` only when the caller knows the app's routes. */
|
|
52
|
+
routes: {
|
|
53
|
+
visited: string[];
|
|
54
|
+
total: number | null;
|
|
55
|
+
unvisited: string[] | null;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/** Lines no scenario reaches, per function: a line counts as covered if any scenario hit it. */
|
|
59
|
+
export declare function aggregateScenarioCoverageGaps(file: ScenarioCoverageFile): ScenarioCoverageGap[];
|
|
60
|
+
/**
|
|
61
|
+
* Everything a scenario measures about the app, read from the meta codegen and
|
|
62
|
+
* `pikku scenario run --coverage` leave in `.pikku`.
|
|
63
|
+
*
|
|
64
|
+
* A mutation is a function a user changes something with: a mutating HTTP
|
|
65
|
+
* route, or an exposed RPC whose name opens with a mutating verb. Only RPCs a
|
|
66
|
+
* scenario can drive are required.
|
|
67
|
+
*/
|
|
68
|
+
export declare function readScenarioCoverage(metaService: MetaService, { routes }?: {
|
|
69
|
+
routes?: string[];
|
|
70
|
+
}): Promise<ScenarioCoverage>;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
const MUTATING_METHODS = new Set(['post', 'put', 'patch', 'delete']);
|
|
2
|
+
const MUTATING_VERB = /^(create|add|new|update|edit|set|change|rename|delete|remove|destroy|archive|restore|assign|unassign|submit|approve|reject|cancel|complete|start|stop|send|invite|upload|register|book|reserve|claim|join|leave|mark|toggle|move|reorder|import|apply|save|record|log|generate|grant|revoke|link|unlink|publish|unpublish|enable|disable|accept|decline|resend|retry|sync|trigger|run|provision|deploy|purchase|checkout|pay|refund|subscribe|unsubscribe|reset|confirm|rotate|attach|detach|duplicate|clone|merge|split|schedule|reschedule|dispatch|finish|close|reopen|promote|demote|transfer|swap|seed|clear|cleanup|refresh|rebuild|install|uninstall|connect|disconnect|activate|deactivate|block|unblock|pin|unpin|vote|rate|comment|reply|post)([A-Z0-9]|$)/;
|
|
3
|
+
const NAVIGATION_STEPS = new Set(['opensPage']);
|
|
4
|
+
const isFrameworkTagged = (tags) => (tags ?? []).some((tag) => tag === 'pikku' || tag.startsWith('pikku:'));
|
|
5
|
+
const isGenerated = (sourceFile) => /\.gen\.ts$/.test(sourceFile ?? '');
|
|
6
|
+
const normalisePath = (path) => path.replace(/\/+$/, '') || '/';
|
|
7
|
+
const pct = (covered, total) => total === 0 ? 100 : Math.round((covered / total) * 100);
|
|
8
|
+
function collapseRanges(lines) {
|
|
9
|
+
const sorted = [...new Set(lines)].sort((a, b) => a - b);
|
|
10
|
+
const ranges = [];
|
|
11
|
+
for (let i = 0; i < sorted.length;) {
|
|
12
|
+
let j = i;
|
|
13
|
+
while (j + 1 < sorted.length && sorted[j + 1] === sorted[j] + 1)
|
|
14
|
+
j++;
|
|
15
|
+
ranges.push(sorted[i] === sorted[j] ? `L${sorted[i]}` : `L${sorted[i]}-${sorted[j]}`);
|
|
16
|
+
i = j + 1;
|
|
17
|
+
}
|
|
18
|
+
return ranges;
|
|
19
|
+
}
|
|
20
|
+
/** Lines no scenario reaches, per function: a line counts as covered if any scenario hit it. */
|
|
21
|
+
export function aggregateScenarioCoverageGaps(file) {
|
|
22
|
+
const byFunction = new Map();
|
|
23
|
+
for (const report of Object.values(file.scenarios ?? {})) {
|
|
24
|
+
for (const fn of report.functions ?? []) {
|
|
25
|
+
if (fn.status === 'unknown' || fn.totalLines === 0)
|
|
26
|
+
continue;
|
|
27
|
+
let entry = byFunction.get(fn.name);
|
|
28
|
+
if (!entry) {
|
|
29
|
+
entry = {
|
|
30
|
+
sourceFile: fn.sourceFile,
|
|
31
|
+
missed: null,
|
|
32
|
+
total: fn.totalLines,
|
|
33
|
+
};
|
|
34
|
+
byFunction.set(fn.name, entry);
|
|
35
|
+
}
|
|
36
|
+
const missed = new Set(fn.missedLines);
|
|
37
|
+
entry.missed =
|
|
38
|
+
entry.missed === null
|
|
39
|
+
? missed
|
|
40
|
+
: new Set([...entry.missed].filter((line) => missed.has(line)));
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const gaps = [];
|
|
44
|
+
for (const [name, entry] of byFunction) {
|
|
45
|
+
if (!entry.missed || entry.missed.size === 0)
|
|
46
|
+
continue;
|
|
47
|
+
const missed = [...entry.missed];
|
|
48
|
+
gaps.push({
|
|
49
|
+
function: name,
|
|
50
|
+
sourceFile: entry.sourceFile,
|
|
51
|
+
status: missed.length >= entry.total ? 'uncovered' : 'partial',
|
|
52
|
+
missing: collapseRanges(missed),
|
|
53
|
+
missedLines: missed.length,
|
|
54
|
+
totalLines: entry.total,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
return gaps.sort((a, b) => (a.status === b.status ? 0 : a.status === 'uncovered' ? -1 : 1) ||
|
|
58
|
+
b.missedLines - a.missedLines ||
|
|
59
|
+
a.function.localeCompare(b.function));
|
|
60
|
+
}
|
|
61
|
+
function scenarioNodes(workflows) {
|
|
62
|
+
return Object.values(workflows).flatMap((meta) => {
|
|
63
|
+
const workflow = meta;
|
|
64
|
+
return workflow?.source === 'scenario'
|
|
65
|
+
? Object.values(workflow.nodes ?? {})
|
|
66
|
+
: [];
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Everything a scenario measures about the app, read from the meta codegen and
|
|
71
|
+
* `pikku scenario run --coverage` leave in `.pikku`.
|
|
72
|
+
*
|
|
73
|
+
* A mutation is a function a user changes something with: a mutating HTTP
|
|
74
|
+
* route, or an exposed RPC whose name opens with a mutating verb. Only RPCs a
|
|
75
|
+
* scenario can drive are required.
|
|
76
|
+
*/
|
|
77
|
+
export async function readScenarioCoverage(metaService, { routes } = {}) {
|
|
78
|
+
const [functions, http, rpc, workflows, coverageFile] = await Promise.all([
|
|
79
|
+
metaService.getFunctionsMeta(),
|
|
80
|
+
metaService.getHttpMeta(),
|
|
81
|
+
metaService.getRpcMeta(),
|
|
82
|
+
metaService.getWorkflowMeta(),
|
|
83
|
+
metaService.readFile('coverage/scenario-coverage.json'),
|
|
84
|
+
]);
|
|
85
|
+
const nodes = scenarioNodes(workflows);
|
|
86
|
+
const driven = new Set(nodes.map((node) => node?.rpcName).filter((name) => !!name));
|
|
87
|
+
const visited = [
|
|
88
|
+
...new Set(nodes
|
|
89
|
+
.filter((node) => node?.rpcName && NAVIGATION_STEPS.has(node.rpcName))
|
|
90
|
+
.map((node) => node.input?.['path'])
|
|
91
|
+
.filter((path) => typeof path === 'string')
|
|
92
|
+
.map(normalisePath)),
|
|
93
|
+
].sort();
|
|
94
|
+
const invocable = new Set(Object.keys(rpc));
|
|
95
|
+
const required = new Map();
|
|
96
|
+
for (const [id, meta] of Object.entries(functions)) {
|
|
97
|
+
const funcId = meta.pikkuFuncId ?? id;
|
|
98
|
+
if (meta.functionType && meta.functionType !== 'user')
|
|
99
|
+
continue;
|
|
100
|
+
if (meta.expose === false || meta.readonly === true)
|
|
101
|
+
continue;
|
|
102
|
+
if (!MUTATING_VERB.test(funcId))
|
|
103
|
+
continue;
|
|
104
|
+
if (isFrameworkTagged(meta.tags) || isGenerated(meta.sourceFile))
|
|
105
|
+
continue;
|
|
106
|
+
if (!invocable.has(funcId))
|
|
107
|
+
continue;
|
|
108
|
+
required.set(funcId, meta.sourceFile);
|
|
109
|
+
}
|
|
110
|
+
for (const [method, wirings] of Object.entries(http)) {
|
|
111
|
+
if (!MUTATING_METHODS.has(method.toLowerCase()))
|
|
112
|
+
continue;
|
|
113
|
+
for (const route of Object.values(wirings ?? {})) {
|
|
114
|
+
const funcId = route?.pikkuFuncId;
|
|
115
|
+
if (!funcId || !invocable.has(funcId))
|
|
116
|
+
continue;
|
|
117
|
+
if (isFrameworkTagged(route.tags))
|
|
118
|
+
continue;
|
|
119
|
+
const sourceFile = functions[funcId]?.sourceFile;
|
|
120
|
+
if (isGenerated(sourceFile))
|
|
121
|
+
continue;
|
|
122
|
+
required.set(funcId, sourceFile);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const uncovered = [...required]
|
|
126
|
+
.filter(([id]) => !driven.has(id))
|
|
127
|
+
.map(([id, sourceFile]) => (sourceFile ? { id, sourceFile } : { id }))
|
|
128
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
129
|
+
let api = null;
|
|
130
|
+
if (coverageFile) {
|
|
131
|
+
const file = JSON.parse(coverageFile);
|
|
132
|
+
const total = Object.values(file.scenarios ?? {}).reduce((most, one) => Math.max(most, one?.summary?.total ?? 0), 0);
|
|
133
|
+
if (total > 0) {
|
|
134
|
+
const gaps = aggregateScenarioCoverageGaps(file);
|
|
135
|
+
api = {
|
|
136
|
+
generatedAt: file.generatedAt,
|
|
137
|
+
environment: file.environment,
|
|
138
|
+
total,
|
|
139
|
+
covered: total - gaps.length,
|
|
140
|
+
pct: pct(total - gaps.length, total),
|
|
141
|
+
gaps,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
const known = routes ? [...new Set(routes.map(normalisePath))].sort() : null;
|
|
146
|
+
return {
|
|
147
|
+
api,
|
|
148
|
+
mutations: {
|
|
149
|
+
required: required.size,
|
|
150
|
+
covered: required.size - uncovered.length,
|
|
151
|
+
uncovered,
|
|
152
|
+
},
|
|
153
|
+
routes: {
|
|
154
|
+
visited,
|
|
155
|
+
total: known ? known.length : null,
|
|
156
|
+
unvisited: known ? known.filter((path) => !visited.includes(path)) : null,
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -1,16 +1,93 @@
|
|
|
1
|
-
import { createHash } from 'node:crypto';
|
|
2
1
|
const PIKKU_WORKFLOW_NAMESPACE = '70696b6b-7500-5770-9f6c-6f77000a0001';
|
|
3
|
-
const
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
const parseUuid = (uuid) => {
|
|
4
|
+
const hex = uuid.replace(/-/g, '');
|
|
5
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
6
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
7
|
+
bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
8
|
+
}
|
|
9
|
+
return bytes;
|
|
10
|
+
};
|
|
4
11
|
const formatUuid = (bytes) => {
|
|
5
|
-
|
|
12
|
+
let hex = '';
|
|
13
|
+
for (const b of bytes)
|
|
14
|
+
hex += b.toString(16).padStart(2, '0');
|
|
6
15
|
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
7
16
|
};
|
|
17
|
+
/**
|
|
18
|
+
* SHA-1 (FIPS 180-4). Web Crypto only offers an async digest and `uuidv5` is
|
|
19
|
+
* called synchronously (and is a public export), so this is a small pure-JS
|
|
20
|
+
* one. It only derives dedupe IDs, never anything secret.
|
|
21
|
+
*/
|
|
22
|
+
const sha1 = (message) => {
|
|
23
|
+
const padded = new Uint8Array(((message.length + 9 + 63) >> 6) << 6);
|
|
24
|
+
padded.set(message);
|
|
25
|
+
padded[message.length] = 0x80;
|
|
26
|
+
const view = new DataView(padded.buffer);
|
|
27
|
+
view.setUint32(padded.length - 8, Math.floor((message.length * 8) / 2 ** 32));
|
|
28
|
+
view.setUint32(padded.length - 4, (message.length * 8) >>> 0);
|
|
29
|
+
let h0 = 0x67452301;
|
|
30
|
+
let h1 = 0xefcdab89;
|
|
31
|
+
let h2 = 0x98badcfe;
|
|
32
|
+
let h3 = 0x10325476;
|
|
33
|
+
let h4 = 0xc3d2e1f0;
|
|
34
|
+
const w = new Uint32Array(80);
|
|
35
|
+
const rotl = (x, n) => (x << n) | (x >>> (32 - n));
|
|
36
|
+
for (let offset = 0; offset < padded.length; offset += 64) {
|
|
37
|
+
for (let i = 0; i < 16; i++)
|
|
38
|
+
w[i] = view.getUint32(offset + i * 4);
|
|
39
|
+
for (let i = 16; i < 80; i++) {
|
|
40
|
+
w[i] = rotl(w[i - 3] ^ w[i - 8] ^ w[i - 14] ^ w[i - 16], 1);
|
|
41
|
+
}
|
|
42
|
+
let a = h0;
|
|
43
|
+
let b = h1;
|
|
44
|
+
let c = h2;
|
|
45
|
+
let d = h3;
|
|
46
|
+
let e = h4;
|
|
47
|
+
for (let i = 0; i < 80; i++) {
|
|
48
|
+
let f;
|
|
49
|
+
let k;
|
|
50
|
+
if (i < 20) {
|
|
51
|
+
f = (b & c) | (~b & d);
|
|
52
|
+
k = 0x5a827999;
|
|
53
|
+
}
|
|
54
|
+
else if (i < 40) {
|
|
55
|
+
f = b ^ c ^ d;
|
|
56
|
+
k = 0x6ed9eba1;
|
|
57
|
+
}
|
|
58
|
+
else if (i < 60) {
|
|
59
|
+
f = (b & c) | (b & d) | (c & d);
|
|
60
|
+
k = 0x8f1bbcdc;
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
f = b ^ c ^ d;
|
|
64
|
+
k = 0xca62c1d6;
|
|
65
|
+
}
|
|
66
|
+
const temp = (rotl(a, 5) + f + e + k + w[i]) >>> 0;
|
|
67
|
+
e = d;
|
|
68
|
+
d = c;
|
|
69
|
+
c = rotl(b, 30) >>> 0;
|
|
70
|
+
b = a;
|
|
71
|
+
a = temp;
|
|
72
|
+
}
|
|
73
|
+
h0 = (h0 + a) >>> 0;
|
|
74
|
+
h1 = (h1 + b) >>> 0;
|
|
75
|
+
h2 = (h2 + c) >>> 0;
|
|
76
|
+
h3 = (h3 + d) >>> 0;
|
|
77
|
+
h4 = (h4 + e) >>> 0;
|
|
78
|
+
}
|
|
79
|
+
const out = new Uint8Array(20);
|
|
80
|
+
const outView = new DataView(out.buffer);
|
|
81
|
+
[h0, h1, h2, h3, h4].forEach((h, i) => outView.setUint32(i * 4, h));
|
|
82
|
+
return out;
|
|
83
|
+
};
|
|
8
84
|
export const uuidv5 = (name, namespace = PIKKU_WORKFLOW_NAMESPACE) => {
|
|
9
|
-
const
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
85
|
+
const ns = parseUuid(namespace);
|
|
86
|
+
const nameBytes = encoder.encode(name);
|
|
87
|
+
const input = new Uint8Array(ns.length + nameBytes.length);
|
|
88
|
+
input.set(ns);
|
|
89
|
+
input.set(nameBytes, ns.length);
|
|
90
|
+
const bytes = sha1(input).subarray(0, 16);
|
|
14
91
|
bytes[6] = (bytes[6] & 0x0f) | 0x50;
|
|
15
92
|
bytes[8] = (bytes[8] & 0x3f) | 0x80;
|
|
16
93
|
return formatUuid(bytes);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/core",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.138",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"./channel": "./dist/wirings/channel/index.js",
|
|
40
40
|
"./workflow": "./dist/wirings/workflow/index.js",
|
|
41
41
|
"./scenario": "./dist/wirings/workflow/pikku-scenario-service.js",
|
|
42
|
+
"./scenario/coverage": "./dist/wirings/workflow/scenario-coverage.js",
|
|
42
43
|
"./workflow/timeline": "./dist/wirings/workflow/run-timeline.js",
|
|
43
44
|
"./workflow/types": "./dist/wirings/workflow/workflow.types.js",
|
|
44
45
|
"./actor-flow": "./dist/wirings/actor-flow/index.js",
|
|
@@ -80,6 +81,7 @@
|
|
|
80
81
|
"./services/local-content": "./dist/services/local-content.js",
|
|
81
82
|
"./services/local-content-request-handler": "./dist/services/local-content-request-handler.js",
|
|
82
83
|
"./services/temporary-file-service": "./dist/services/temporary-file-service.js",
|
|
84
|
+
"./services/file-scenario-run-store": "./dist/services/file-scenario-run-store.js",
|
|
83
85
|
"./crypto-utils": "./dist/crypto-utils.js",
|
|
84
86
|
"./hmac": "./dist/utils/hmac.js",
|
|
85
87
|
"./state": "./dist/pikku-state.js",
|
|
@@ -99,6 +101,7 @@
|
|
|
99
101
|
},
|
|
100
102
|
"devDependencies": {
|
|
101
103
|
"@ag-ui/core": "0.0.57",
|
|
104
|
+
"@pikku/deploy": "^0.12.15",
|
|
102
105
|
"@types/node": "^24.13.3",
|
|
103
106
|
"esbuild": "~0.28.2",
|
|
104
107
|
"tsx": "^4.23.12"
|
package/src/public-surface.json
CHANGED
|
@@ -99,6 +99,10 @@
|
|
|
99
99
|
"resolveFeatureScenarios",
|
|
100
100
|
"resolveScenarioSurfaces"
|
|
101
101
|
],
|
|
102
|
+
"./scenario/coverage": [
|
|
103
|
+
"aggregateScenarioCoverageGaps",
|
|
104
|
+
"readScenarioCoverage"
|
|
105
|
+
],
|
|
102
106
|
"./workflow/timeline": [
|
|
103
107
|
"buildRunTimeline",
|
|
104
108
|
"reconstructFinalState",
|
|
@@ -322,6 +326,7 @@
|
|
|
322
326
|
"validateAndBuildCredentialDefinitionsMeta"
|
|
323
327
|
],
|
|
324
328
|
"./scope": [
|
|
329
|
+
"declareScopes",
|
|
325
330
|
"defineScope",
|
|
326
331
|
"flattenScopeDefinitions",
|
|
327
332
|
"hasScopes",
|
|
@@ -428,7 +433,6 @@
|
|
|
428
433
|
"ACTOR_SECRET_NAME",
|
|
429
434
|
"ConsoleLogger",
|
|
430
435
|
"DEFAULT_WEBHOOK_RETRIES",
|
|
431
|
-
"FileScenarioRunStore",
|
|
432
436
|
"InMemoryAgentRunStateService",
|
|
433
437
|
"InMemoryLeaseService",
|
|
434
438
|
"InMemoryQueueService",
|
|
@@ -478,8 +482,6 @@
|
|
|
478
482
|
"pikkuRemoteScheduledJobFunc",
|
|
479
483
|
"pikkuWebhookWorkerFunc",
|
|
480
484
|
"renderEmail",
|
|
481
|
-
"scenarioArtifactContentType",
|
|
482
|
-
"scenarioRunSummary",
|
|
483
485
|
"spy",
|
|
484
486
|
"stub",
|
|
485
487
|
"verifyActorSecret",
|
|
@@ -506,6 +508,11 @@
|
|
|
506
508
|
"createLocalContentRequestHandler",
|
|
507
509
|
"verifySignedContentRequest"
|
|
508
510
|
],
|
|
511
|
+
"./services/file-scenario-run-store": [
|
|
512
|
+
"FileScenarioRunStore",
|
|
513
|
+
"scenarioArtifactContentType",
|
|
514
|
+
"scenarioRunSummary"
|
|
515
|
+
],
|
|
509
516
|
"./services/temporary-file-service": [
|
|
510
517
|
"TemporaryFileInstance",
|
|
511
518
|
"TemporaryFileService"
|