@antprofuse/saddle-islands-tools 0.0.0-stage → 0.1.3

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.
Files changed (119) hide show
  1. package/package.json +15 -4
  2. package/tools/adapters/islands/trigger_source.py +53 -0
  3. package/tools/cli/backend.mjs +24 -0
  4. package/tools/cli/main.py +35 -0
  5. package/tools/cli/minifish.mjs +38 -0
  6. package/tools/cli/minifish.py +52 -0
  7. package/tools/core/package-identity.mjs +23 -0
  8. package/tools/core/package_identity.py +58 -0
  9. package/tools/core/semantic_identity.py +149 -0
  10. package/tools/core/test_semantic_identity.py +136 -0
  11. package/tools/docs/README.md +42 -0
  12. package/tools/docs/backend-testing.md +36 -0
  13. package/tools/docs/minifish-testing.md +32 -0
  14. package/tools/docs/semantic-identity.md +61 -0
  15. package/tools/modules/backend/actions.mjs +10 -0
  16. package/tools/modules/backend/assert.mjs +20 -0
  17. package/tools/modules/backend/context-receiver.py +30 -0
  18. package/tools/modules/backend/facility.test.mjs +44 -0
  19. package/tools/modules/backend/grouped_context.py +100 -0
  20. package/tools/modules/backend/grpc.mjs +57 -0
  21. package/tools/modules/backend/index.mjs +9 -0
  22. package/tools/modules/backend/input-tree.mjs +14 -0
  23. package/tools/modules/backend/logs.mjs +24 -0
  24. package/tools/modules/backend/logs.test.mjs +28 -0
  25. package/tools/modules/backend/metadata.mjs +66 -0
  26. package/tools/modules/backend/package-lock.json +362 -0
  27. package/tools/modules/backend/package.json +15 -0
  28. package/tools/modules/backend/paths.mjs +11 -0
  29. package/tools/modules/backend/product.mjs +48 -0
  30. package/tools/modules/backend/rejection-logs.mjs +33 -0
  31. package/tools/modules/backend/report.mjs +13 -0
  32. package/tools/modules/backend/report.test.mjs +13 -0
  33. package/tools/modules/backend/run.mjs +50 -0
  34. package/tools/modules/backend/util.mjs +24 -0
  35. package/tools/modules/minifish/checks/guard.test.mjs +13 -0
  36. package/tools/modules/minifish/checks/package.test.mjs +23 -0
  37. package/tools/modules/minifish/compiler/build-case.mjs +39 -0
  38. package/tools/modules/minifish/compiler/case-plan.mjs +175 -0
  39. package/tools/modules/minifish/compiler/config.mjs +29 -0
  40. package/tools/modules/minifish/compiler/observation-module-name.mjs +2 -0
  41. package/tools/modules/minifish/compiler/package-runner.mjs +43 -0
  42. package/tools/modules/minifish/compiler/semantic-module-gate.mjs +27 -0
  43. package/tools/modules/minifish/compiler/tool-identity.mjs +14 -0
  44. package/tools/modules/minifish/harness/batch_cases.py +91 -0
  45. package/tools/modules/minifish/harness/batch_plugin_groups.py +56 -0
  46. package/tools/modules/minifish/harness/batch_report.py +104 -0
  47. package/tools/modules/minifish/harness/browser.mjs +146 -0
  48. package/tools/modules/minifish/harness/build-plugin-artifact.mjs +25 -0
  49. package/tools/modules/minifish/harness/capture_contract.py +78 -0
  50. package/tools/modules/minifish/harness/carousel-observation.mjs +62 -0
  51. package/tools/modules/minifish/harness/carriers.mjs +49 -0
  52. package/tools/modules/minifish/harness/check-independent-batch.py +84 -0
  53. package/tools/modules/minifish/harness/check-runner-isolation.py +102 -0
  54. package/tools/modules/minifish/harness/component_runtime.py +125 -0
  55. package/tools/modules/minifish/harness/contract-value.mjs +32 -0
  56. package/tools/modules/minifish/harness/deferred-visual.mjs +21 -0
  57. package/tools/modules/minifish/harness/device-prepare.mjs +26 -0
  58. package/tools/modules/minifish/harness/execution-evidence.mjs +37 -0
  59. package/tools/modules/minifish/harness/foreground.mjs +20 -0
  60. package/tools/modules/minifish/harness/header-layout.mjs +47 -0
  61. package/tools/modules/minifish/harness/held-snapshot.mjs +67 -0
  62. package/tools/modules/minifish/harness/host-device-fixture.mjs +28 -0
  63. package/tools/modules/minifish/harness/host-language.mjs +73 -0
  64. package/tools/modules/minifish/harness/host-native-ui.mjs +39 -0
  65. package/tools/modules/minifish/harness/host-navigation.mjs +96 -0
  66. package/tools/modules/minifish/harness/image-loading.mjs +35 -0
  67. package/tools/modules/minifish/harness/independent-case-runner.mjs +175 -0
  68. package/tools/modules/minifish/harness/independent-visual.mjs +107 -0
  69. package/tools/modules/minifish/harness/inline-svg.mjs +32 -0
  70. package/tools/modules/minifish/harness/input-lifecycle-evidence.mjs +16 -0
  71. package/tools/modules/minifish/harness/jsapi.mjs +92 -0
  72. package/tools/modules/minifish/harness/loading-input.mjs +52 -0
  73. package/tools/modules/minifish/harness/loading-observation.mjs +25 -0
  74. package/tools/modules/minifish/harness/local-navigation-evidence.mjs +82 -0
  75. package/tools/modules/minifish/harness/miniapp-screenshot.mjs +38 -0
  76. package/tools/modules/minifish/harness/native-dom-sampling.mjs +40 -0
  77. package/tools/modules/minifish/harness/native-enabled.mjs +11 -0
  78. package/tools/modules/minifish/harness/native-rpc-failure.mjs +11 -0
  79. package/tools/modules/minifish/harness/native-state.mjs +14 -0
  80. package/tools/modules/minifish/harness/native-ui-contract.mjs +29 -0
  81. package/tools/modules/minifish/harness/navigation.mjs +19 -0
  82. package/tools/modules/minifish/harness/network_isolation.py +134 -0
  83. package/tools/modules/minifish/harness/painted-source-image.mjs +54 -0
  84. package/tools/modules/minifish/harness/playwright.mjs +1 -0
  85. package/tools/modules/minifish/harness/plugin-events.mjs +16 -0
  86. package/tools/modules/minifish/harness/plugin-package.mjs +23 -0
  87. package/tools/modules/minifish/harness/plugin_artifact.py +23 -0
  88. package/tools/modules/minifish/harness/product_identity.py +8 -0
  89. package/tools/modules/minifish/harness/project_config.py +14 -0
  90. package/tools/modules/minifish/harness/prototype-visual.mjs +123 -0
  91. package/tools/modules/minifish/harness/region-order.mjs +16 -0
  92. package/tools/modules/minifish/harness/renderer-image-evidence.mjs +32 -0
  93. package/tools/modules/minifish/harness/request-guard.mjs +65 -0
  94. package/tools/modules/minifish/harness/request-plan.mjs +124 -0
  95. package/tools/modules/minifish/harness/response-consumption.mjs +106 -0
  96. package/tools/modules/minifish/harness/retained-page-lifecycle.mjs +48 -0
  97. package/tools/modules/minifish/harness/rpc-wire.mjs +34 -0
  98. package/tools/modules/minifish/harness/run-batch.py +133 -0
  99. package/tools/modules/minifish/harness/run-platform.py +293 -0
  100. package/tools/modules/minifish/harness/runtime_isolation.py +71 -0
  101. package/tools/modules/minifish/harness/scroll-container-observation.mjs +40 -0
  102. package/tools/modules/minifish/harness/semantic-anchor.mjs +139 -0
  103. package/tools/modules/minifish/harness/semantic-observation.mjs +197 -0
  104. package/tools/modules/minifish/harness/skeleton-appearance.mjs +49 -0
  105. package/tools/modules/minifish/harness/target-actions.mjs +120 -0
  106. package/tools/modules/minifish/harness/test_batch_concurrency.py +125 -0
  107. package/tools/modules/minifish/harness/test_batch_plugin_groups.py +97 -0
  108. package/tools/modules/minifish/harness/test_batch_runner.py +107 -0
  109. package/tools/modules/minifish/harness/test_common_report.py +47 -0
  110. package/tools/modules/minifish/harness/test_held_capture_contract.py +50 -0
  111. package/tools/modules/minifish/harness/test_pty_tail.py +39 -0
  112. package/tools/modules/minifish/harness/test_runtime_dependencies.py +49 -0
  113. package/tools/modules/minifish/harness/zero-business-plan.mjs +19 -0
  114. package/tools/modules/minifish/identity.py +6 -0
  115. package/tools/modules/minifish/package-lock.json +74 -0
  116. package/tools/modules/minifish/package.json +10 -0
  117. package/tools/modules/minifish/sdk/package-lock.json +57745 -0
  118. package/tools/modules/minifish/sdk/package.json +13 -0
  119. 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
+ }