@antprofuse/saddle-islands-tools 0.0.0-stage → 0.1.4
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/package.json +15 -4
- package/tools/adapters/islands/trigger_source.py +53 -0
- package/tools/cli/backend.mjs +24 -0
- package/tools/cli/main.py +38 -0
- package/tools/cli/minifish.mjs +38 -0
- package/tools/cli/minifish.py +52 -0
- package/tools/core/package-identity.mjs +23 -0
- package/tools/core/package_identity.py +58 -0
- package/tools/core/semantic_identity.py +149 -0
- package/tools/core/test_semantic_identity.py +136 -0
- package/tools/docs/README.md +43 -0
- package/tools/docs/backend-testing.md +36 -0
- package/tools/docs/iteration.md +34 -0
- package/tools/docs/minifish-testing.md +32 -0
- package/tools/docs/semantic-identity.md +61 -0
- package/tools/modules/backend/actions.mjs +10 -0
- package/tools/modules/backend/assert.mjs +20 -0
- package/tools/modules/backend/context-receiver.py +30 -0
- package/tools/modules/backend/facility.test.mjs +44 -0
- package/tools/modules/backend/grouped_context.py +100 -0
- package/tools/modules/backend/grpc.mjs +57 -0
- package/tools/modules/backend/index.mjs +9 -0
- package/tools/modules/backend/input-tree.mjs +14 -0
- package/tools/modules/backend/logs.mjs +24 -0
- package/tools/modules/backend/logs.test.mjs +28 -0
- package/tools/modules/backend/metadata.mjs +66 -0
- package/tools/modules/backend/package-lock.json +362 -0
- package/tools/modules/backend/package.json +15 -0
- package/tools/modules/backend/paths.mjs +11 -0
- package/tools/modules/backend/product.mjs +48 -0
- package/tools/modules/backend/rejection-logs.mjs +33 -0
- package/tools/modules/backend/report.mjs +13 -0
- package/tools/modules/backend/report.test.mjs +13 -0
- package/tools/modules/backend/run.mjs +50 -0
- package/tools/modules/backend/util.mjs +24 -0
- package/tools/modules/minifish/checks/guard.test.mjs +13 -0
- package/tools/modules/minifish/checks/package.test.mjs +23 -0
- package/tools/modules/minifish/compiler/build-case.mjs +39 -0
- package/tools/modules/minifish/compiler/case-plan.mjs +175 -0
- package/tools/modules/minifish/compiler/config.mjs +29 -0
- package/tools/modules/minifish/compiler/observation-module-name.mjs +2 -0
- package/tools/modules/minifish/compiler/package-runner.mjs +43 -0
- package/tools/modules/minifish/compiler/semantic-module-gate.mjs +27 -0
- package/tools/modules/minifish/compiler/tool-identity.mjs +14 -0
- package/tools/modules/minifish/harness/batch_cases.py +91 -0
- package/tools/modules/minifish/harness/batch_plugin_groups.py +56 -0
- package/tools/modules/minifish/harness/batch_report.py +104 -0
- package/tools/modules/minifish/harness/browser.mjs +146 -0
- package/tools/modules/minifish/harness/build-plugin-artifact.mjs +25 -0
- package/tools/modules/minifish/harness/capture_contract.py +78 -0
- package/tools/modules/minifish/harness/carousel-observation.mjs +62 -0
- package/tools/modules/minifish/harness/carriers.mjs +49 -0
- package/tools/modules/minifish/harness/check-independent-batch.py +84 -0
- package/tools/modules/minifish/harness/check-runner-isolation.py +102 -0
- package/tools/modules/minifish/harness/component_runtime.py +125 -0
- package/tools/modules/minifish/harness/contract-value.mjs +32 -0
- package/tools/modules/minifish/harness/deferred-visual.mjs +21 -0
- package/tools/modules/minifish/harness/device-prepare.mjs +26 -0
- package/tools/modules/minifish/harness/execution-evidence.mjs +37 -0
- package/tools/modules/minifish/harness/foreground.mjs +20 -0
- package/tools/modules/minifish/harness/header-layout.mjs +47 -0
- package/tools/modules/minifish/harness/held-snapshot.mjs +67 -0
- package/tools/modules/minifish/harness/host-device-fixture.mjs +28 -0
- package/tools/modules/minifish/harness/host-language.mjs +73 -0
- package/tools/modules/minifish/harness/host-native-ui.mjs +39 -0
- package/tools/modules/minifish/harness/host-navigation.mjs +96 -0
- package/tools/modules/minifish/harness/image-loading.mjs +35 -0
- package/tools/modules/minifish/harness/independent-case-runner.mjs +175 -0
- package/tools/modules/minifish/harness/independent-visual.mjs +107 -0
- package/tools/modules/minifish/harness/inline-svg.mjs +32 -0
- package/tools/modules/minifish/harness/input-lifecycle-evidence.mjs +16 -0
- package/tools/modules/minifish/harness/jsapi.mjs +92 -0
- package/tools/modules/minifish/harness/loading-input.mjs +52 -0
- package/tools/modules/minifish/harness/loading-observation.mjs +25 -0
- package/tools/modules/minifish/harness/local-navigation-evidence.mjs +82 -0
- package/tools/modules/minifish/harness/miniapp-screenshot.mjs +38 -0
- package/tools/modules/minifish/harness/native-dom-sampling.mjs +40 -0
- package/tools/modules/minifish/harness/native-enabled.mjs +11 -0
- package/tools/modules/minifish/harness/native-rpc-failure.mjs +11 -0
- package/tools/modules/minifish/harness/native-state.mjs +14 -0
- package/tools/modules/minifish/harness/native-ui-contract.mjs +29 -0
- package/tools/modules/minifish/harness/navigation.mjs +19 -0
- package/tools/modules/minifish/harness/network_isolation.py +134 -0
- package/tools/modules/minifish/harness/painted-source-image.mjs +54 -0
- package/tools/modules/minifish/harness/playwright.mjs +1 -0
- package/tools/modules/minifish/harness/plugin-events.mjs +16 -0
- package/tools/modules/minifish/harness/plugin-package.mjs +23 -0
- package/tools/modules/minifish/harness/plugin_artifact.py +23 -0
- package/tools/modules/minifish/harness/product_identity.py +8 -0
- package/tools/modules/minifish/harness/project_config.py +14 -0
- package/tools/modules/minifish/harness/prototype-visual.mjs +123 -0
- package/tools/modules/minifish/harness/region-order.mjs +16 -0
- package/tools/modules/minifish/harness/renderer-image-evidence.mjs +32 -0
- package/tools/modules/minifish/harness/request-guard.mjs +65 -0
- package/tools/modules/minifish/harness/request-plan.mjs +124 -0
- package/tools/modules/minifish/harness/response-consumption.mjs +106 -0
- package/tools/modules/minifish/harness/retained-page-lifecycle.mjs +48 -0
- package/tools/modules/minifish/harness/rpc-wire.mjs +34 -0
- package/tools/modules/minifish/harness/run-batch.py +133 -0
- package/tools/modules/minifish/harness/run-platform.py +293 -0
- package/tools/modules/minifish/harness/runtime_isolation.py +71 -0
- package/tools/modules/minifish/harness/scroll-container-observation.mjs +40 -0
- package/tools/modules/minifish/harness/semantic-anchor.mjs +139 -0
- package/tools/modules/minifish/harness/semantic-observation.mjs +197 -0
- package/tools/modules/minifish/harness/skeleton-appearance.mjs +49 -0
- package/tools/modules/minifish/harness/target-actions.mjs +120 -0
- package/tools/modules/minifish/harness/test_batch_concurrency.py +125 -0
- package/tools/modules/minifish/harness/test_batch_plugin_groups.py +97 -0
- package/tools/modules/minifish/harness/test_batch_runner.py +107 -0
- package/tools/modules/minifish/harness/test_common_report.py +47 -0
- package/tools/modules/minifish/harness/test_held_capture_contract.py +50 -0
- package/tools/modules/minifish/harness/test_pty_tail.py +39 -0
- package/tools/modules/minifish/harness/test_runtime_dependencies.py +49 -0
- package/tools/modules/minifish/harness/zero-business-plan.mjs +19 -0
- package/tools/modules/minifish/identity.py +6 -0
- package/tools/modules/minifish/package-lock.json +74 -0
- package/tools/modules/minifish/package.json +10 -0
- package/tools/modules/minifish/sdk/package-lock.json +57745 -0
- package/tools/modules/minifish/sdk/package.json +13 -0
- package/tools/modules/workflow/diff_review.py +74 -0
- package/tools/modules/workflow/iteration.py +175 -0
- package/README.md +0 -3
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {spawnSync} from 'node:child_process';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
export const contextKeys=['trace-info-bin','ldc-info-bin','request-info-bin','profuse-context-bin'];
|
|
5
|
+
const receiver=fileURLToPath(new URL('./context-receiver.py',import.meta.url));
|
|
6
|
+
// Read every native value, including duplicates. Never synthesize absent groups.
|
|
7
|
+
export function observeMetadata(metadata,deadline){
|
|
8
|
+
const raw=[];
|
|
9
|
+
for(const key of contextKeys)for(const value of metadata.get(key)){
|
|
10
|
+
assert(Buffer.isBuffer(value),`${key}: native binary required`);
|
|
11
|
+
raw.push([key,value.toString('hex')]);
|
|
12
|
+
}
|
|
13
|
+
const decoded=spawnSync('python3',['-B',receiver],{input:JSON.stringify(raw),encoding:'utf8'});
|
|
14
|
+
if(decoded.status!==0){const error=new Error(`invalid grouped metadata: ${decoded.stderr||decoded.error||''}`);error.contextRaw=raw;throw error;}
|
|
15
|
+
const groups=JSON.parse(decoded.stdout);
|
|
16
|
+
return {raw,groups,grpcDeadline:deadline instanceof Date?deadline.getTime():deadline};
|
|
17
|
+
}
|
|
18
|
+
export function assertMetadata(records,expectations){
|
|
19
|
+
// Ownership comes from the expected protobuf plan, never from wire metadata.
|
|
20
|
+
assert(Array.isArray(expectations),'independent expected call ownership required');
|
|
21
|
+
const plans=new Map(),owners=new Map();
|
|
22
|
+
for(const owner of expectations){
|
|
23
|
+
assert(!owners.has(owner.ownerId),'duplicate expected owner');owners.set(owner.ownerId,owner);
|
|
24
|
+
for(const plan of owner.plans){assert(!plans.has(plan.planId),'duplicate expected plan');plans.set(plan.planId,{...plan,owner});}
|
|
25
|
+
}
|
|
26
|
+
const used=new Set(),checked=[];
|
|
27
|
+
for(const record of records){
|
|
28
|
+
const plan=plans.get(record.planId);assert(plan,'unassigned gRPC record');
|
|
29
|
+
assert.equal(record.ownerId,plan.owner.ownerId,'record ownership differs from plan');
|
|
30
|
+
assert(!used.has(record.planId),'extra record for expected call');used.add(record.planId);
|
|
31
|
+
assert.equal(record.method,plan.method,'planned gRPC method');
|
|
32
|
+
}
|
|
33
|
+
for(const [id,plan]of plans)assert(used.has(id)||plan.optional,'missing planned metadata call');
|
|
34
|
+
for(const owner of owners.values()){
|
|
35
|
+
const own=records.filter(record=>plans.get(record.planId).owner===owner);
|
|
36
|
+
if(!own.length)continue;
|
|
37
|
+
assert(owner.ingress,'ingress must be registered before the RPC');
|
|
38
|
+
checked.push(...checkOwner(own,owner.ingress));
|
|
39
|
+
}
|
|
40
|
+
assert.equal(checked.length,records.length,'all actual records must be checked');
|
|
41
|
+
return checked;
|
|
42
|
+
}
|
|
43
|
+
function checkOwner(own,ingress){
|
|
44
|
+
const sequences=[];
|
|
45
|
+
for(const record of own){
|
|
46
|
+
assert(record.context,'missing observed context');
|
|
47
|
+
const {groups,grpcDeadline}=record.context;
|
|
48
|
+
const trace=groups['trace-info-bin'],req=groups['request-info-bin'];
|
|
49
|
+
assert(trace.rpcId.startsWith(ingress.traceInfo.rpcId+'.'),'child rpc identity');
|
|
50
|
+
const suffix=trace.rpcId.slice(ingress.traceInfo.rpcId.length+1);
|
|
51
|
+
assert(/^[1-9][0-9]*$/.test(suffix),'positive child sequence');
|
|
52
|
+
sequences.push(Number(suffix));
|
|
53
|
+
assert.deepEqual(trace,{...ingress.traceInfo,rpcId:ingress.traceInfo.rpcId+'.'+suffix});
|
|
54
|
+
assert.deepEqual(groups['ldc-info-bin'],ingress.ldcInfo,'LDC including unknown extensions');
|
|
55
|
+
assert.deepEqual(req,{requestId:ingress.requestId,callId:ingress.callId+'-'+suffix,deadlineUnixMs:req.deadlineUnixMs});
|
|
56
|
+
assert(Number.isSafeInteger(req.deadlineUnixMs)&&req.deadlineUnixMs>ingress.startedAt,'valid active absolute deadline');
|
|
57
|
+
assert(req.deadlineUnixMs<=ingress.deadline,'deadline extended');
|
|
58
|
+
// Native gRPC timeout is relative at receipt; allow transit and millisecond rounding.
|
|
59
|
+
assert(Number.isFinite(grpcDeadline)&&Math.abs(grpcDeadline-req.deadlineUnixMs)<100,'grpc timeout differs from shared deadline');
|
|
60
|
+
assert.equal(Object.hasOwn(groups,'profuse-context-bin'),Object.hasOwn(ingress,'profuseContext'),'missing versus empty profuseContext');
|
|
61
|
+
if(Object.hasOwn(ingress,'profuseContext'))assert.deepEqual(groups['profuse-context-bin'],ingress.profuseContext);
|
|
62
|
+
}
|
|
63
|
+
assert.equal(new Set(sequences).size,sequences.length,'reused child identity');
|
|
64
|
+
assert.deepEqual([...sequences].sort((a,b)=>a-b),Array.from({length:sequences.length},(_,i)=>i+1));
|
|
65
|
+
return own;
|
|
66
|
+
}
|
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saddle-backend-test-tools",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"lockfileVersion": 3,
|
|
5
|
+
"requires": true,
|
|
6
|
+
"packages": {
|
|
7
|
+
"": {
|
|
8
|
+
"name": "saddle-backend-test-tools",
|
|
9
|
+
"version": "0.1.4",
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@grpc/grpc-js": "1.14.4",
|
|
12
|
+
"@grpc/proto-loader": "0.8.0",
|
|
13
|
+
"protobufjs": "7.5.4",
|
|
14
|
+
"yaml": "2.8.1"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"node_modules/@grpc/grpc-js": {
|
|
18
|
+
"version": "1.14.4",
|
|
19
|
+
"resolved": "https://registry.npmmirror.com/@grpc/grpc-js/-/grpc-js-1.14.4.tgz",
|
|
20
|
+
"integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==",
|
|
21
|
+
"license": "Apache-2.0",
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"@grpc/proto-loader": "^0.8.0",
|
|
24
|
+
"@js-sdsl/ordered-map": "^4.4.2"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=12.10.0"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
"node_modules/@grpc/proto-loader": {
|
|
31
|
+
"version": "0.8.0",
|
|
32
|
+
"resolved": "https://registry.npmmirror.com/@grpc/proto-loader/-/proto-loader-0.8.0.tgz",
|
|
33
|
+
"integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==",
|
|
34
|
+
"license": "Apache-2.0",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"lodash.camelcase": "^4.3.0",
|
|
37
|
+
"long": "^5.0.0",
|
|
38
|
+
"protobufjs": "^7.5.3",
|
|
39
|
+
"yargs": "^17.7.2"
|
|
40
|
+
},
|
|
41
|
+
"bin": {
|
|
42
|
+
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=6"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"node_modules/@js-sdsl/ordered-map": {
|
|
49
|
+
"version": "4.4.2",
|
|
50
|
+
"resolved": "https://registry.npmmirror.com/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
|
51
|
+
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
|
52
|
+
"license": "MIT",
|
|
53
|
+
"funding": {
|
|
54
|
+
"type": "opencollective",
|
|
55
|
+
"url": "https://opencollective.com/js-sdsl"
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"node_modules/@protobufjs/aspromise": {
|
|
59
|
+
"version": "1.1.2",
|
|
60
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
|
61
|
+
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
|
62
|
+
"license": "BSD-3-Clause"
|
|
63
|
+
},
|
|
64
|
+
"node_modules/@protobufjs/base64": {
|
|
65
|
+
"version": "1.1.2",
|
|
66
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/base64/-/base64-1.1.2.tgz",
|
|
67
|
+
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
|
68
|
+
"license": "BSD-3-Clause"
|
|
69
|
+
},
|
|
70
|
+
"node_modules/@protobufjs/codegen": {
|
|
71
|
+
"version": "2.0.5",
|
|
72
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
|
73
|
+
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
|
74
|
+
"license": "BSD-3-Clause"
|
|
75
|
+
},
|
|
76
|
+
"node_modules/@protobufjs/eventemitter": {
|
|
77
|
+
"version": "1.1.1",
|
|
78
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
|
79
|
+
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
|
80
|
+
"license": "BSD-3-Clause"
|
|
81
|
+
},
|
|
82
|
+
"node_modules/@protobufjs/fetch": {
|
|
83
|
+
"version": "1.1.1",
|
|
84
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
|
85
|
+
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
|
86
|
+
"license": "BSD-3-Clause",
|
|
87
|
+
"dependencies": {
|
|
88
|
+
"@protobufjs/aspromise": "^1.1.1"
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
"node_modules/@protobufjs/float": {
|
|
92
|
+
"version": "1.0.2",
|
|
93
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/float/-/float-1.0.2.tgz",
|
|
94
|
+
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
|
95
|
+
"license": "BSD-3-Clause"
|
|
96
|
+
},
|
|
97
|
+
"node_modules/@protobufjs/inquire": {
|
|
98
|
+
"version": "1.1.2",
|
|
99
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/inquire/-/inquire-1.1.2.tgz",
|
|
100
|
+
"integrity": "sha512-pa0vFRuws4wkvaXKK1uXZMAwAX4/t8ANaJo45iw/oQHNQ9q5xUzwgFmVJGXiga2BeN+zpX7Vf9vmsiIa2J+MUw==",
|
|
101
|
+
"license": "BSD-3-Clause"
|
|
102
|
+
},
|
|
103
|
+
"node_modules/@protobufjs/path": {
|
|
104
|
+
"version": "1.1.2",
|
|
105
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/path/-/path-1.1.2.tgz",
|
|
106
|
+
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
|
107
|
+
"license": "BSD-3-Clause"
|
|
108
|
+
},
|
|
109
|
+
"node_modules/@protobufjs/pool": {
|
|
110
|
+
"version": "1.1.0",
|
|
111
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/pool/-/pool-1.1.0.tgz",
|
|
112
|
+
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
|
113
|
+
"license": "BSD-3-Clause"
|
|
114
|
+
},
|
|
115
|
+
"node_modules/@protobufjs/utf8": {
|
|
116
|
+
"version": "1.1.2",
|
|
117
|
+
"resolved": "https://registry.npmmirror.com/@protobufjs/utf8/-/utf8-1.1.2.tgz",
|
|
118
|
+
"integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
|
|
119
|
+
"license": "BSD-3-Clause"
|
|
120
|
+
},
|
|
121
|
+
"node_modules/@types/node": {
|
|
122
|
+
"version": "26.6.5",
|
|
123
|
+
"resolved": "https://registry.npmmirror.com/@types/node/-/node-26.6.5.tgz",
|
|
124
|
+
"integrity": "sha512-uMzsv9stDdofIZBx4UGh4Z9ZgXnGC9seRDscilBcTHAeOFk67MZg/k9bWVfO+bk4qZmG+l52hRnLwUiH2LnJTg==",
|
|
125
|
+
"license": "MIT",
|
|
126
|
+
"dependencies": {
|
|
127
|
+
"undici-types": "~8.9.0"
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
"node_modules/ansi-regex": {
|
|
131
|
+
"version": "5.0.1",
|
|
132
|
+
"resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
|
133
|
+
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
|
134
|
+
"license": "MIT",
|
|
135
|
+
"engines": {
|
|
136
|
+
"node": ">=8"
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
"node_modules/ansi-styles": {
|
|
140
|
+
"version": "4.3.0",
|
|
141
|
+
"resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
142
|
+
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
143
|
+
"license": "MIT",
|
|
144
|
+
"dependencies": {
|
|
145
|
+
"color-convert": "^2.0.1"
|
|
146
|
+
},
|
|
147
|
+
"engines": {
|
|
148
|
+
"node": ">=8"
|
|
149
|
+
},
|
|
150
|
+
"funding": {
|
|
151
|
+
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
|
152
|
+
}
|
|
153
|
+
},
|
|
154
|
+
"node_modules/cliui": {
|
|
155
|
+
"version": "8.0.1",
|
|
156
|
+
"resolved": "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz",
|
|
157
|
+
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
|
158
|
+
"license": "ISC",
|
|
159
|
+
"dependencies": {
|
|
160
|
+
"string-width": "^4.2.0",
|
|
161
|
+
"strip-ansi": "^6.0.1",
|
|
162
|
+
"wrap-ansi": "^7.0.0"
|
|
163
|
+
},
|
|
164
|
+
"engines": {
|
|
165
|
+
"node": ">=12"
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
"node_modules/color-convert": {
|
|
169
|
+
"version": "2.0.1",
|
|
170
|
+
"resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
|
|
171
|
+
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
172
|
+
"license": "MIT",
|
|
173
|
+
"dependencies": {
|
|
174
|
+
"color-name": "~1.1.4"
|
|
175
|
+
},
|
|
176
|
+
"engines": {
|
|
177
|
+
"node": ">=7.0.0"
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
"node_modules/color-name": {
|
|
181
|
+
"version": "1.1.4",
|
|
182
|
+
"resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
|
|
183
|
+
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
|
184
|
+
"license": "MIT"
|
|
185
|
+
},
|
|
186
|
+
"node_modules/emoji-regex": {
|
|
187
|
+
"version": "8.0.0",
|
|
188
|
+
"resolved": "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
|
189
|
+
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
|
190
|
+
"license": "MIT"
|
|
191
|
+
},
|
|
192
|
+
"node_modules/escalade": {
|
|
193
|
+
"version": "3.2.0",
|
|
194
|
+
"resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
|
|
195
|
+
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
|
196
|
+
"license": "MIT",
|
|
197
|
+
"engines": {
|
|
198
|
+
"node": ">=6"
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
"node_modules/get-caller-file": {
|
|
202
|
+
"version": "2.0.5",
|
|
203
|
+
"resolved": "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
|
204
|
+
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
|
205
|
+
"license": "ISC",
|
|
206
|
+
"engines": {
|
|
207
|
+
"node": "6.* || 8.* || >= 10.*"
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
"node_modules/is-fullwidth-code-point": {
|
|
211
|
+
"version": "3.0.0",
|
|
212
|
+
"resolved": "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
|
213
|
+
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
|
214
|
+
"license": "MIT",
|
|
215
|
+
"engines": {
|
|
216
|
+
"node": ">=8"
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
"node_modules/lodash.camelcase": {
|
|
220
|
+
"version": "4.3.0",
|
|
221
|
+
"resolved": "https://registry.npmmirror.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
|
222
|
+
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
|
223
|
+
"license": "MIT"
|
|
224
|
+
},
|
|
225
|
+
"node_modules/long": {
|
|
226
|
+
"version": "5.3.2",
|
|
227
|
+
"resolved": "https://registry.npmmirror.com/long/-/long-5.3.2.tgz",
|
|
228
|
+
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
|
229
|
+
"license": "Apache-2.0"
|
|
230
|
+
},
|
|
231
|
+
"node_modules/protobufjs": {
|
|
232
|
+
"version": "7.5.4",
|
|
233
|
+
"resolved": "https://registry.npmmirror.com/protobufjs/-/protobufjs-7.5.4.tgz",
|
|
234
|
+
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
|
235
|
+
"hasInstallScript": true,
|
|
236
|
+
"license": "BSD-3-Clause",
|
|
237
|
+
"dependencies": {
|
|
238
|
+
"@protobufjs/aspromise": "^1.1.2",
|
|
239
|
+
"@protobufjs/base64": "^1.1.2",
|
|
240
|
+
"@protobufjs/codegen": "^2.0.4",
|
|
241
|
+
"@protobufjs/eventemitter": "^1.1.0",
|
|
242
|
+
"@protobufjs/fetch": "^1.1.0",
|
|
243
|
+
"@protobufjs/float": "^1.0.2",
|
|
244
|
+
"@protobufjs/inquire": "^1.1.0",
|
|
245
|
+
"@protobufjs/path": "^1.1.2",
|
|
246
|
+
"@protobufjs/pool": "^1.1.0",
|
|
247
|
+
"@protobufjs/utf8": "^1.1.0",
|
|
248
|
+
"@types/node": ">=13.7.0",
|
|
249
|
+
"long": "^5.0.0"
|
|
250
|
+
},
|
|
251
|
+
"engines": {
|
|
252
|
+
"node": ">=12.0.0"
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
"node_modules/require-directory": {
|
|
256
|
+
"version": "2.1.1",
|
|
257
|
+
"resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz",
|
|
258
|
+
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
|
259
|
+
"license": "MIT",
|
|
260
|
+
"engines": {
|
|
261
|
+
"node": ">=0.10.0"
|
|
262
|
+
}
|
|
263
|
+
},
|
|
264
|
+
"node_modules/string-width": {
|
|
265
|
+
"version": "4.2.3",
|
|
266
|
+
"resolved": "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz",
|
|
267
|
+
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
|
268
|
+
"license": "MIT",
|
|
269
|
+
"dependencies": {
|
|
270
|
+
"emoji-regex": "^8.0.0",
|
|
271
|
+
"is-fullwidth-code-point": "^3.0.0",
|
|
272
|
+
"strip-ansi": "^6.0.1"
|
|
273
|
+
},
|
|
274
|
+
"engines": {
|
|
275
|
+
"node": ">=8"
|
|
276
|
+
}
|
|
277
|
+
},
|
|
278
|
+
"node_modules/strip-ansi": {
|
|
279
|
+
"version": "6.0.1",
|
|
280
|
+
"resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
|
281
|
+
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
|
282
|
+
"license": "MIT",
|
|
283
|
+
"dependencies": {
|
|
284
|
+
"ansi-regex": "^5.0.1"
|
|
285
|
+
},
|
|
286
|
+
"engines": {
|
|
287
|
+
"node": ">=8"
|
|
288
|
+
}
|
|
289
|
+
},
|
|
290
|
+
"node_modules/undici-types": {
|
|
291
|
+
"version": "8.9.0",
|
|
292
|
+
"resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-8.9.0.tgz",
|
|
293
|
+
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
|
294
|
+
"license": "MIT"
|
|
295
|
+
},
|
|
296
|
+
"node_modules/wrap-ansi": {
|
|
297
|
+
"version": "7.0.0",
|
|
298
|
+
"resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
|
299
|
+
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
|
300
|
+
"license": "MIT",
|
|
301
|
+
"dependencies": {
|
|
302
|
+
"ansi-styles": "^4.0.0",
|
|
303
|
+
"string-width": "^4.1.0",
|
|
304
|
+
"strip-ansi": "^6.0.0"
|
|
305
|
+
},
|
|
306
|
+
"engines": {
|
|
307
|
+
"node": ">=10"
|
|
308
|
+
},
|
|
309
|
+
"funding": {
|
|
310
|
+
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
|
311
|
+
}
|
|
312
|
+
},
|
|
313
|
+
"node_modules/y18n": {
|
|
314
|
+
"version": "5.0.8",
|
|
315
|
+
"resolved": "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz",
|
|
316
|
+
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
|
317
|
+
"license": "ISC",
|
|
318
|
+
"engines": {
|
|
319
|
+
"node": ">=10"
|
|
320
|
+
}
|
|
321
|
+
},
|
|
322
|
+
"node_modules/yaml": {
|
|
323
|
+
"version": "2.8.1",
|
|
324
|
+
"resolved": "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz",
|
|
325
|
+
"integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==",
|
|
326
|
+
"license": "ISC",
|
|
327
|
+
"bin": {
|
|
328
|
+
"yaml": "bin.mjs"
|
|
329
|
+
},
|
|
330
|
+
"engines": {
|
|
331
|
+
"node": ">= 14.6"
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
"node_modules/yargs": {
|
|
335
|
+
"version": "17.7.3",
|
|
336
|
+
"resolved": "https://registry.npmmirror.com/yargs/-/yargs-17.7.3.tgz",
|
|
337
|
+
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
|
|
338
|
+
"license": "MIT",
|
|
339
|
+
"dependencies": {
|
|
340
|
+
"cliui": "^8.0.1",
|
|
341
|
+
"escalade": "^3.1.1",
|
|
342
|
+
"get-caller-file": "^2.0.5",
|
|
343
|
+
"require-directory": "^2.1.1",
|
|
344
|
+
"string-width": "^4.2.3",
|
|
345
|
+
"y18n": "^5.0.5",
|
|
346
|
+
"yargs-parser": "^21.1.1"
|
|
347
|
+
},
|
|
348
|
+
"engines": {
|
|
349
|
+
"node": ">=12"
|
|
350
|
+
}
|
|
351
|
+
},
|
|
352
|
+
"node_modules/yargs-parser": {
|
|
353
|
+
"version": "21.1.1",
|
|
354
|
+
"resolved": "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
|
355
|
+
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
|
356
|
+
"license": "ISC",
|
|
357
|
+
"engines": {
|
|
358
|
+
"node": ">=12"
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "saddle-backend-test-tools",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"@grpc/grpc-js": "1.14.4",
|
|
8
|
+
"@grpc/proto-loader": "0.8.0",
|
|
9
|
+
"protobufjs": "7.5.4",
|
|
10
|
+
"yaml": "2.8.1"
|
|
11
|
+
},
|
|
12
|
+
"scripts": {
|
|
13
|
+
"selftest": "node --test *.test.mjs"
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {realpath,access} from 'node:fs/promises';
|
|
3
|
+
import {resolve,relative,isAbsolute} from 'node:path';
|
|
4
|
+
export async function within(root,file,label='dependency'){
|
|
5
|
+
const base=await realpath(resolve(root)),target=await realpath(resolve(file)),rel=relative(base,target);
|
|
6
|
+
assert(rel!== '..'&&!rel.startsWith('../')&&!isAbsolute(rel),`${label} escapes declared root`);return target;
|
|
7
|
+
}
|
|
8
|
+
export async function preflight({projectRoot,repositoryRoot,adapter,proto,binary,list=false}){
|
|
9
|
+
await within(repositoryRoot,projectRoot,'project');await within(projectRoot,adapter,'adapter');await within(repositoryRoot,proto,'protocol');
|
|
10
|
+
if(!list)await access(resolve(binary));
|
|
11
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {writeFile,mkdir,cp} from 'node:fs/promises';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import {randomUUID} from 'node:crypto';
|
|
4
|
+
import assert from 'node:assert/strict';
|
|
5
|
+
import {port,processStart,until} from './util.mjs';
|
|
6
|
+
export async function product({binary,app,alias,db,mock,out,business='',encodeRequest=(_request,body)=>JSON.stringify(body),databaseConcurrency=4,rpcConcurrency=4,timeout=5000}) {
|
|
7
|
+
assert(binary,'missing real product executable --binary');assert(app,'--app must be explicit deployment_app');assert(/^[A-Za-z0-9_]+$/.test(alias),'invalid grpc deployment alias');
|
|
8
|
+
await mkdir(out,{recursive:true});if(db)await cp(db.mappings,resolve(out,'mappings'),{recursive:true});const http=await port(),management=await port();
|
|
9
|
+
const config=`[framework]
|
|
10
|
+
listen = "127.0.0.1:${http}"
|
|
11
|
+
[framework.management]
|
|
12
|
+
bind = "127.0.0.1:${management}"
|
|
13
|
+
[framework.lifecycle]
|
|
14
|
+
startTimeoutMs = 30000
|
|
15
|
+
shutdownTimeoutMs = 10000
|
|
16
|
+
[framework.admission]
|
|
17
|
+
cpuCores = 2
|
|
18
|
+
memoryMb = 256
|
|
19
|
+
requestTimeoutMs = ${timeout}
|
|
20
|
+
[framework.admission.dependencies]
|
|
21
|
+
databaseConcurrency = ${databaseConcurrency}
|
|
22
|
+
profusecontractConcurrency = ${rpcConcurrency}
|
|
23
|
+
[framework.grpc.${alias}]
|
|
24
|
+
authority = "http://127.0.0.1:${mock.port}"
|
|
25
|
+
[framework.observability.logging]
|
|
26
|
+
directory = "${resolve(out,'logs')}"
|
|
27
|
+
rotation = "daily"
|
|
28
|
+
${db?`[secrets]
|
|
29
|
+
databaseUrlEnv = "BT_PRIVATE_DATABASE_URL"
|
|
30
|
+
[database]
|
|
31
|
+
mappingDir = "mappings"
|
|
32
|
+
`:'[secrets]\n'}${business}`;
|
|
33
|
+
|
|
34
|
+
const configPath=resolve(out,'saddle.toml');await writeFile(configPath,config);
|
|
35
|
+
const proc=await processStart(resolve(binary),['--config',configPath],out,{...(db?{BT_PRIVATE_DATABASE_URL:db.url}:{}),SADDLE_BOOTSTRAP_LOG_DIRECTORY:resolve(out,'bootstrap')});
|
|
36
|
+
try{await until(async()=>{assert.equal(proc.child.exitCode,null,'product exited during startup');const r=await fetch(`http://127.0.0.1:${management}/ready`);assert.equal(r.status,200);assert.deepEqual(await r.json(),{ready:true});},30000);}catch(e){await proc.stop();throw e;}
|
|
37
|
+
return {proc,http,management,async call(operation,request,user='synthetic-user-A',options={}){
|
|
38
|
+
const id=randomUUID(),callId=randomUUID(),startedAt=Date.now(),deadline=options.deadline??startedAt+timeout;const body={target:{app:options.app??app,interfaceId:operation},profuseGwContext:{userInfo:{userId:user},traceInfo:{traceId:id,rpcId:'0'},ldcInfo:{zone:'test',idc:'local',env:'test'}},requestData:request};
|
|
39
|
+
if(options.traceInfo)body.profuseGwContext.traceInfo=options.traceInfo;
|
|
40
|
+
if(options.ldcInfo)body.profuseGwContext.ldcInfo=options.ldcInfo;
|
|
41
|
+
if(Object.hasOwn(options,'profuseContext'))body.profuseContext=options.profuseContext;
|
|
42
|
+
const ingress={requestId:id,callId,startedAt,deadline,traceInfo:body.profuseGwContext.traceInfo,ldcInfo:body.profuseGwContext.ldcInfo,...(Object.hasOwn(body,'profuseContext')?{profuseContext:body.profuseContext}:{})};
|
|
43
|
+
mock.registerIngress(ingress);
|
|
44
|
+
const requestRaw=options.rawBody??encodeRequest(request,body,options);
|
|
45
|
+
const r=await fetch(`http://127.0.0.1:${http}/saddle/v1/ingress/profusegw/invoke`,{method:'POST',headers:{'Content-Type':'application/json','X-Request-Id':id,'X-Call-Id':callId,'X-Deadline-Unix-Ms':String(deadline)},body:requestRaw,signal:AbortSignal.any([AbortSignal.timeout(timeout+5000),...(options.signal?[options.signal]:[])])});
|
|
46
|
+
const raw=await r.text();let response=null;try{if(raw)response=JSON.parse(raw);}catch(e){throw Error(`invalid JSON HTTP response status ${r.status}: ${e.message}`);}return {status:r.status,body:response,raw,request:body,requestRaw,ingress,completedAt:Date.now()};
|
|
47
|
+
},async close(){await proc.stop();assert.equal((await proc.done).code,0,'product did not finish a clean shutdown');}};
|
|
48
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
const value=(c,k)=>c?.[k]?.state==='present'?c[k].value:undefined;
|
|
3
|
+
const identity=e=>e.request_identity??e.context?.requestInfo?.requestId??value(e.context,'request');
|
|
4
|
+
export function assertDecodeRejection(logs,actual,{operation,emergency=[],calls=[]}={}){
|
|
5
|
+
assert.equal(actual.status,400,'decode rejection HTTP400');assert.equal(calls.length,0,'decode rejection made RPC');
|
|
6
|
+
const i=actual.ingress,owned=logs.filter(e=>identity(e)===i.requestId);
|
|
7
|
+
const stages=owned.filter(e=>e.recordSchema==='1.0'&&e.context?.schemaVersion==='1.0');
|
|
8
|
+
assert(stages.length,'missing schema1 request context');
|
|
9
|
+
const locals=new Set(),tasks=new Set();
|
|
10
|
+
for(const e of stages){const c=e.context;assert.equal(c.requestInfo.callId,i.callId,'request call ownership');assert.equal(c.traceInfo.traceId,i.traceInfo.traceId);assert.equal(c.traceInfo.rpcId,i.traceInfo.rpcId);assert.equal(c.interfaceInfo.interfaceId,operation);assert.equal(c.executionInfo.phase,'reading','decode rejection entered execution');assert.equal(c._meta.fieldStates['/executionInfo/dbOperation'],'not_applicable');assert.equal(c._meta.fieldStates['/executionInfo/scopeId'],'not_established');locals.add(c.requestInfo.localRequestId);tasks.add(c.executionInfo.taskId);}
|
|
11
|
+
assert.equal(locals.size,1);assert.equal(tasks.size,1);
|
|
12
|
+
const records=emergency.filter(e=>identity(e)===i.requestId);
|
|
13
|
+
const bound=records.filter(e=>e.event==='request_account_bound'&&e.schema==='request-account-identity/1');assert.equal(bound.length,1,'missing/duplicate account binding');
|
|
14
|
+
const b=bound[0];assert(Number.isInteger(b.ledger_instance)&&b.ledger_instance>0,'missing ledger instance');const span=value(b.context,'span_id');assert(span,'missing owned span');
|
|
15
|
+
const check=e=>{const c=e.context;assert.equal(c.schema_version,2,'account context schema');assert.equal(String(c.local_request),[...locals][0],'local request ownership');assert.equal(value(c,'request'),i.requestId);assert.equal(value(c,'trace_id'),i.traceInfo.traceId);assert.equal(value(c,'rpc_id'),i.traceInfo.rpcId);assert.equal(value(c,'span_id'),span,'span ownership');assert.equal(value(c,'operation'),operation);assert.equal(String(value(c,'task')),[...tasks][0]);assert.equal(value(c,'lifecycle'),'reading');assert.equal(c.db_operation.state,'not_applicable');assert.equal(c.scope.state,'not_established');};
|
|
16
|
+
for(const e of records)check(e);
|
|
17
|
+
const related=logs.filter(e=>identity(e)===i.requestId||(e.trace_id===i.traceInfo.traceId&&e.span_id===span));
|
|
18
|
+
for(const e of related){if(identity(e)!==undefined)assert.equal(identity(e),i.requestId,'cross-request span');if(e.trace_id!==undefined)assert.equal(e.trace_id,i.traceInfo.traceId);if(e.rpc_id!==undefined)assert.equal(e.rpc_id,i.traceInfo.rpcId);assert(!['handler','database','profuse_contract','grpc'].includes(e.call_kind)&&!['handler','database','profuse_contract'].includes(e.stage),'decode rejection entered handler/DB/RPC');}
|
|
19
|
+
const root=related.filter(e=>e.call_kind==='external_request');
|
|
20
|
+
for(const event of ['framework.call.started','framework.call.finished'])assert.equal(root.filter(e=>e.event===event).length,1,'missing/duplicate root '+event);
|
|
21
|
+
for(const e of root)assert.equal(e.operation,operation);
|
|
22
|
+
const finish=root.find(e=>e.event==='framework.call.finished');assert.equal(finish.error_code,'service.ingress.input');assert.equal(finish.error_kind,'invalid_argument');assert.equal(finish.outcome,'failure');
|
|
23
|
+
const reclaimed=records.filter(e=>e.schema==='request-finalization/1'&&e.event==='request_task_reclaimed');assert.equal(reclaimed.length,1,'missing/duplicate task reclaim');
|
|
24
|
+
const released=records.filter(e=>e.schema==='request-finalization/1'&&e.event==='request_account_released');assert.equal(released.length,1,'missing physical release');
|
|
25
|
+
for(const e of [...reclaimed,...released]){assert.equal(e.process_instance,b.process_instance,'process ledger ownership');assert.equal(e.process_id,b.pid);for(const k of ['account_slot','account_generation','audit_generation'])assert.equal(e.account[k],b.account[k],'account ledger ownership '+k);}
|
|
26
|
+
assert.equal(reclaimed[0].completion,'failed');assert.equal(released[0].completion,'physically_released');assert.equal(released[0].account.healthy,true);
|
|
27
|
+
const boundaries=records.filter(e=>e.event==='request_failure_boundary');assert.equal(boundaries.length,1,'missing source rejection boundary');const boundary=boundaries[0];check({context:boundary.source_context});assert.equal(boundary.source_submission,'written');assert.equal(boundary.original_capture,'complete_written');
|
|
28
|
+
const originals=emergency.filter(e=>e.event==='request_error_original'&&e.channel==='context'&&e.state==='field_end'&&e.occurrence?.diagnostic_id===boundary.occurrence.diagnostic_id);assert.equal(originals.length,1,'missing source context');
|
|
29
|
+
const source=JSON.parse(originals[0].payload);assert.equal(source.context.requestInfo.requestId,i.requestId);assert.equal(source.context.requestInfo.callId,i.callId);assert.equal(source.context.traceInfo.traceId,i.traceInfo.traceId);assert.equal(source.context.traceInfo.rpcId,i.traceInfo.rpcId);assert.equal(source.context.requestInfo.localRequestId,[...locals][0]);assert.equal(source.context.executionInfo.taskId,[...tasks][0]);assert.equal(source.context.executionInfo.phase,'reading');assert.equal(source.facts.causes[0].stage,'request_decode');
|
|
30
|
+
const audits=emergency.filter(e=>e.event==='request_audit_passed'&&e.state==='record_complete').map(e=>JSON.parse(e.payload)).filter(e=>e.context?.requestInfo?.requestId===i.requestId);assert.equal(audits.length,1,'missing completed resource audit');const audit=audits[0];assert.equal(audit.context.requestInfo.callId,i.callId);assert.equal(audit.context.requestInfo.localRequestId,[...locals][0]);assert.equal(audit.context.traceInfo.traceId,i.traceInfo.traceId);assert.equal(audit.context.traceInfo.rpcId,i.traceInfo.rpcId);assert.equal(audit.context.executionInfo.phase,'reading');assert.equal(audit.context.executionInfo.taskId,[...tasks][0]);for(const k of ['account_slot','account_generation','audit_generation'])assert.equal(audit.audit[k],b.account[k],'audit account ownership');for(const k of ['outstanding_bytes','outstanding_objects','database_connections','database_operations','rpc_operations'])assert.equal(audit.audit[k],0,'resource audit '+k);assert.equal(audit.audit.storage_occupied,false);
|
|
31
|
+
return {phase:'input_decode_rejected',db:'not_applicable',scope:'not_established',reclaimed:true,physicallyReleased:true,events:related,account:b.account};
|
|
32
|
+
}
|
|
33
|
+
export function assertRouteRejection(actual,calls){assert.equal(actual.status,404,'wrong-app must be HTTP404');assert.notEqual(actual.body?.success,true);assert.equal(calls.length,0,'wrong-app made RPC');return {phase:'route_before_identity',rpc:'independent_mock_zero',requestReclamation:'unverified',handlerAttribution:'unverified',verified:false};}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function classifyReport(report,selected){
|
|
2
|
+
const frameworkIds=new Set(selected.filter(s=>s.scope==='framework').map(s=>s.id));
|
|
3
|
+
const observations=report.scenarios.filter(s=>frameworkIds.has(s.id));
|
|
4
|
+
const business=selected.filter(s=>!frameworkIds.has(s.id));
|
|
5
|
+
report.scenarios=report.scenarios.filter(s=>!frameworkIds.has(s.id));
|
|
6
|
+
report.total=business.length;
|
|
7
|
+
for(const status of ['passed','failed','unverified'])report[status]=report.scenarios.filter(s=>s.status===status).length;
|
|
8
|
+
report.notRunIds=business.filter(s=>!report.scenarios.some(x=>x.id===s.id)).map(s=>s.id);
|
|
9
|
+
report.notRun=report.notRunIds.length;
|
|
10
|
+
report.framework={total:frameworkIds.size,observations,notRunIds:[...frameworkIds].filter(id=>!observations.some(s=>s.id===id)),scope:'outside-Spec-business-acceptance'};
|
|
11
|
+
report.status=report.infrastructureError||report.cleanupErrors?'infrastructure-failed':report.failed?'failed':report.notRun||report.unverified?'incomplete':'passed';
|
|
12
|
+
return report;
|
|
13
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import {classifyReport} from './report.mjs';
|
|
4
|
+
test('framework observations do not count as business acceptance or hide business failures',()=>{
|
|
5
|
+
const selected=[{id:'business'},{id:'wrong-app',scope:'framework'}];
|
|
6
|
+
for(const status of ['unverified','failed']){
|
|
7
|
+
const r=classifyReport({scenarios:[{id:'business',status:'passed'},{id:'wrong-app',status}]},selected);
|
|
8
|
+
assert.equal(r.total,1);assert.equal(r.passed,1);assert.equal(r.failed,0);assert.equal(r.unverified,0);assert.equal(r.status,'passed');assert.equal(r.framework.observations[0].status,status);
|
|
9
|
+
}
|
|
10
|
+
assert.equal(classifyReport({scenarios:[{id:'business',status:'failed'}]},selected).status,'failed');
|
|
11
|
+
assert.equal(classifyReport({scenarios:[]},selected).status,'incomplete');
|
|
12
|
+
assert.equal(classifyReport({scenarios:[{id:'business',status:'passed'}],cleanupErrors:[{}]},selected).status,'infrastructure-failed');
|
|
13
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import {resolve} from 'node:path';
|
|
3
|
+
import {pathToFileURL} from 'node:url';
|
|
4
|
+
import {mkdir,readFile,writeFile,access} from 'node:fs/promises';
|
|
5
|
+
import {constants} from 'node:fs';
|
|
6
|
+
import {createHash} from 'node:crypto';
|
|
7
|
+
import {within,preflight} from './paths.mjs';
|
|
8
|
+
import {scenarioActions} from './actions.mjs';
|
|
9
|
+
import {mock} from './grpc.mjs';
|
|
10
|
+
import {product} from './product.mjs';
|
|
11
|
+
import {externalOutput,sleep} from './util.mjs';
|
|
12
|
+
import {requestLogs} from './logs.mjs';
|
|
13
|
+
import {assertRouteRejection} from './rejection-logs.mjs';
|
|
14
|
+
import {classifyReport} from './report.mjs';
|
|
15
|
+
export async function run({projectRoot,repositoryRoot,adapter,out,binary,caseId,list=false,identity}){
|
|
16
|
+
assert(projectRoot&&repositoryRoot&&adapter,'explicit project root, repository root and adapter required');
|
|
17
|
+
await within(repositoryRoot,projectRoot,'project');await within(projectRoot,adapter,'adapter');
|
|
18
|
+
const {project}=await import(pathToFileURL(resolve(adapter)));const config=await project(resolve(projectRoot));
|
|
19
|
+
assert(config.rpc?.serviceName&&config.proto&&config.process?.app&&config.process?.alias,'incomplete project protocol/process configuration');
|
|
20
|
+
assert.equal(typeof config.prepare,'function','controlled scenario plan required');
|
|
21
|
+
assert.equal(typeof config.execute,'function','controlled scenario action required');
|
|
22
|
+
await preflight({projectRoot,repositoryRoot,adapter,proto:config.proto,binary,list});
|
|
23
|
+
assert(Array.isArray(config.inputFiles)&&config.inputFiles.length,'explicit project input file closure required');
|
|
24
|
+
const projectInputs=[];for(const file of [...new Set([resolve(adapter),resolve(config.proto),...config.inputFiles.map(file=>resolve(file))])]){await within(repositoryRoot,file,'project input');projectInputs.push({file:file.slice(resolve(repositoryRoot).length+1),sha256:createHash('sha256').update(await readFile(file)).digest('hex')});}
|
|
25
|
+
const all=config.scenarios;assert(Array.isArray(all)&&all.length,'empty scenarios');assert.equal(new Set(all.map(s=>s.id)).size,all.length,'duplicate scene id');
|
|
26
|
+
const selected=caseId?all.filter(s=>s.id===caseId):all;assert(selected.length,'unknown case');
|
|
27
|
+
if(list){await new Promise(done=>process.stdout.write(JSON.stringify(all.map(({id,operation,basis,scope})=>({id,operation,basis,scope:scope??'business'})),null,2)+'\n',done));return;}
|
|
28
|
+
assert(binary&&out,'real binary and fresh external output required');await access(resolve(binary),constants.X_OK);await access(config.proto);const destination=externalOutput(repositoryRoot,out);
|
|
29
|
+
await mkdir(destination);const report={...config.frozen,tool:identity,projectInputs,scenarioSetSha256:createHash('sha256').update(JSON.stringify(all)).digest('hex'),started:new Date().toISOString(),scenarios:[],database:config.database};
|
|
30
|
+
let rpc,app,stage='startup';const abort=new AbortController();const listeners=new Map();
|
|
31
|
+
for(const signal of ['SIGINT','SIGTERM']){const handler=()=>{report.interrupted=signal;abort.abort();rpc?.release();};listeners.set(signal,handler);process.once(signal,handler);}
|
|
32
|
+
try{
|
|
33
|
+
report.binarySha256=createHash('sha256').update(await readFile(resolve(binary))).digest('hex');
|
|
34
|
+
rpc=await mock(config.proto,{...config.rpc,requireContext:true});
|
|
35
|
+
app=await product({...config.process,binary:resolve(binary),mock:rpc,out:resolve(destination,'product')});stage='business';
|
|
36
|
+
for(const s of selected){if(abort.signal.aborted)break;const dir=resolve(destination,s.id.replace(/[^\w.-]/g,'_'));await mkdir(dir);const e={id:s.id,basis:s.basis},pending=new Set();
|
|
37
|
+
try{
|
|
38
|
+
// Adapter receives only controlled actions. Every HTTP action registers a
|
|
39
|
+
// strict planned owner, every RPC uses wire/schema/parameter/count guards.
|
|
40
|
+
const controlled=scenarioActions({plan:config.prepare(s),rpc,app,abort,pending});
|
|
41
|
+
const result=await config.execute(s,{rpc:controlled.rpc,app:controlled.app,out:destination,abort,e,track:p=>{pending.add(p);p.catch(()=>{});},requestLogs:(directory,actual,options)=>requestLogs(directory,actual,{...options,database:config.databaseLogPolicy}),assertRouteRejection,sleep});
|
|
42
|
+
controlled.finish();
|
|
43
|
+
assert(['passed','unverified'].includes(result?.status),'invalid scenario result');report.scenarios.push({id:s.id,...result});
|
|
44
|
+
}catch(err){e.error=err.stack;report.scenarios.push({id:s.id,status:'failed',error:err.message});}
|
|
45
|
+
finally{rpc.release();await Promise.allSettled([...pending]);e.calls=structuredClone(rpc.records);e.ownership=rpc.expectations();e.violations=[...rpc.violations];await writeFile(resolve(dir,'evidence.json'),JSON.stringify(e,null,2));}
|
|
46
|
+
}
|
|
47
|
+
}catch(err){report.failureStage=stage;report.infrastructureError=err.stack;}
|
|
48
|
+
finally{for(const [name,resource]of [['product',app],['grpc',rpc]])if(resource)try{await resource.close();}catch(err){(report.cleanupErrors??=[]).push({name,error:err.message});}for(const [signal,handler]of listeners)process.removeListener(signal,handler);report.finished=new Date().toISOString();classifyReport(report,selected);await writeFile(resolve(destination,'report.json'),JSON.stringify(report,null,2));}
|
|
49
|
+
console.log(JSON.stringify(report));process.exitCode=report.status==='passed'?0:1;return report;
|
|
50
|
+
}
|