arcane-os 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/README.md +86 -117
- package/bin/arcane-test.mjs +170 -46
- package/browser-runtime/ai/browser-speech-artifacts.mjs +855 -895
- package/browser-runtime/ai/browser-speech-providers.mjs +80 -204
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +627 -819
- package/browser-runtime/ai/browser-wasm.mjs +24 -35
- package/browser-runtime/ai/browser-wllama-runtime.mjs +64 -316
- package/browser-runtime/ai/model-controller.mjs +584 -181
- package/browser-runtime/ai/speech-worker-client.mjs +8 -148
- package/browser-runtime/ai/speech-worker-runtime.mjs +642 -374
- package/browser-runtime/dom-event-instrumentation.mjs +55 -147
- package/browser-runtime/event-manager.mjs +239 -624
- package/package.json +5 -6
- package/runtime/arcane/components/app-bar.html +3 -15
- package/runtime/arcane/components/assistant-panel.html +10 -10
- package/runtime/arcane/components/calculator.html +1 -1
- package/runtime/arcane/components/chat.html +1359 -135
- package/runtime/arcane/components/conversation-view.html +2 -2
- package/runtime/arcane/components/document-inspector.html +11 -17
- package/runtime/arcane/components/file-manager.html +13 -56
- package/runtime/arcane/components/markdown-document.html +82 -281
- package/runtime/arcane/components/markdown-editor.html +7 -10
- package/runtime/arcane/components/media-embed.html +6 -6
- package/runtime/arcane/components/screen-capture.html +4 -4
- package/runtime/arcane/components/source-explanation.html +2 -2
- package/runtime/arcane/components/speech.html +112 -68
- package/runtime/arcane/components/terminal-workspace.html +4 -4
- package/runtime/arcane/components/theme-editor.html +1 -1
- package/runtime/arcane/components/unified-inbox.html +2 -2
- package/runtime/arcane/components/voice-transcription.html +31 -21
- package/runtime/arcane/entities/Calculation.js +2 -3
- package/runtime/arcane/entities/Chat.js +228 -43
- package/runtime/arcane/entities/Preference.js +3 -5
- package/runtime/arcane/entities/Weather.js +5 -5
- package/runtime/arcane/modules/AI.js +1042 -427
- package/runtime/arcane/modules/AIProviderRuntime.js +618 -359
- package/runtime/arcane/modules/AIResponseLength.js +9 -19
- package/runtime/arcane/modules/AIRuntimeState.js +109 -72
- package/runtime/arcane/modules/ArcaneNavigationPolicy.js +45 -32
- package/runtime/arcane/modules/BrowserTestSuite.js +78 -122
- package/runtime/arcane/modules/CalculatorEngine.js +9 -9
- package/runtime/arcane/modules/CommunicationAppController.js +3 -7
- package/runtime/arcane/modules/ComponentContracts.js +30 -32
- package/runtime/arcane/modules/ConfiguredAIChatSession.js +281 -230
- package/runtime/arcane/modules/ConversationActionItems.js +26 -59
- package/runtime/arcane/modules/ConversationClosingReport.js +34 -61
- package/runtime/arcane/modules/ConversationTimebox.js +27 -15
- package/runtime/arcane/modules/DBOPFSDocumentLibrary.js +152 -344
- package/runtime/arcane/modules/DocumentLexicalSearch.js +25 -91
- package/runtime/arcane/modules/HTMLImport.js +54 -1
- package/runtime/arcane/modules/IsolatedModelQuestionRunner.js +40 -203
- package/runtime/arcane/modules/LocalAIReadiness.js +40 -60
- package/runtime/arcane/modules/LocalAIReadinessController.js +15 -13
- package/runtime/arcane/modules/MD.js +1 -45
- package/runtime/arcane/modules/Mail.js +51 -103
- package/runtime/arcane/modules/MailOutbox.mjs +95 -193
- package/runtime/arcane/modules/MailTransport.mjs +36 -57
- package/runtime/arcane/modules/ModelDefinition.js +22 -106
- package/runtime/arcane/modules/OpenMeteoWeatherProvider.js +39 -101
- package/runtime/arcane/modules/PersistentAIChatSession.js +281 -18
- package/runtime/arcane/modules/PreferenceStore.js +102 -30
- package/runtime/arcane/modules/RiskSignalAnalyzer.js +8 -9
- package/runtime/arcane/modules/ScopedOPFSCache.js +7 -42
- package/runtime/arcane/modules/ScreenCapture.js +175 -128
- package/runtime/arcane/modules/SpeechPlayback.js +46 -149
- package/runtime/arcane/modules/StaticDocumentCatalog.js +173 -407
- package/runtime/arcane/modules/ToolCallRouter.js +25 -12
- package/runtime/arcane/modules/YouTubeMedia.js +6 -5
- package/schemas/arcane-app-bundle.schema.json +13 -78
- package/schemas/arcane-app.schema.json +9 -25
- package/schemas/arcane-lock.schema.json +18 -151
- package/schemas/arcane-package.schema.json +2 -16
- package/schemas/native-build-plan.schema.json +119 -122
- package/src/app-descriptor.mjs +75 -132
- package/src/application-tests.mjs +200 -0
- package/src/cli/main.mjs +27 -46
- package/src/constants.mjs +3 -4
- package/src/dev-server.mjs +30 -324
- package/src/doctor.mjs +92 -154
- package/src/dom-event-instrumentation.mjs +55 -147
- package/src/errors.mjs +2 -3
- package/src/event-manager.mjs +239 -624
- package/src/event-queue.mjs +3 -3
- package/src/import-map.mjs +273 -1028
- package/src/index.mjs +14 -16
- package/src/installed-sdk-runtime.mjs +27 -67
- package/src/integrated-provider-loader.mjs +53 -382
- package/src/mail-api.mjs +0 -2
- package/src/mail-server.mjs +224 -580
- package/src/mail.mjs +4 -10
- package/src/native-plan.mjs +163 -598
- package/src/native-provider-loader.mjs +104 -1063
- package/src/packager/core.mjs +485 -3229
- package/src/process.mjs +5 -10
- package/src/release-bundle.mjs +292 -2405
- package/src/runtime.mjs +76 -396
- package/src/scaffold.mjs +30 -80
- package/src/sdk-browser-runtime.mjs +70 -626
- package/src/source-server.mjs +588 -0
- package/src/targets/index.mjs +78 -188
- package/src/templates/workspace-template.mjs +19 -135
- package/src/testing-loader.mjs +164 -0
- package/src/testing.mjs +1 -1
- package/src/toolchain.mjs +131 -544
- package/src/update-check.mjs +26 -64
- package/src/workspace-operation-lock.mjs +139 -430
- package/src/workspace-runtime.mjs +109 -1558
- package/src/workspace.mjs +40 -302
- package/browser-runtime/ARCANE_SDK_BROWSER_RELEASE.json +0 -218
- package/browser-runtime/ai/ARCANE_AI_BROWSER_SPEECH_COMPONENTS.json +0 -203
- package/browser-runtime/ai/ARCANE_AI_BROWSER_WASM_COMPONENTS.json +0 -80
- package/browser-runtime/ai/internal/sha256.mjs +0 -166
- package/docs/architecture.md +0 -344
- package/docs/compatibility.md +0 -36
- package/docs/event-manager.md +0 -294
- package/docs/platform-targets.md +0 -108
- package/docs/publishing.md +0 -201
- package/docs/reference/README.md +0 -185
- package/docs/reference/ai/browser-speech-package-authority.json +0 -835
- package/docs/reference/ai/browser-speech.md +0 -1295
- package/docs/reference/ai/browser-wasm.md +0 -530
- package/docs/reference/arcane-ollama.md +0 -288
- package/docs/reference/availability-and-normalization.md +0 -183
- package/docs/reference/behavioral-testing.md +0 -133
- package/docs/reference/cli.md +0 -779
- package/docs/reference/core/README.md +0 -62
- package/docs/reference/core/arcane-ai-contracts.md +0 -907
- package/docs/reference/core/arcane-api.md +0 -601
- package/docs/reference/core/arcane-entities.md +0 -65
- package/docs/reference/core/arcane-events.md +0 -134
- package/docs/reference/core/ollama-module.md +0 -181
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +0 -1909
- package/docs/reference/core/reference/arcane-api/applications-terminal-capabilities.md +0 -1057
- package/docs/reference/core/reference/arcane-api/core-and-events.md +0 -320
- package/docs/reference/core/reference/arcane-api/filesystem-storage-preferences-appearance.md +0 -610
- package/docs/reference/core/reference/arcane-api/namespaces.md +0 -1157
- package/docs/reference/core/reference/arcane-api/platform-installation-users-system.md +0 -1423
- package/docs/reference/core/reference/arcane-api/session-provisioning-diagnostics-development.md +0 -315
- package/docs/reference/event-manager.md +0 -1511
- package/docs/reference/inventory/package-api.json +0 -3284
- package/docs/reference/inventory/runtime-components.json +0 -1011
- package/docs/reference/inventory/runtime-entities.json +0 -26
- package/docs/reference/inventory/runtime-modules.json +0 -1431
- package/docs/reference/mail.md +0 -316
- package/docs/reference/protocols.md +0 -719
- package/docs/reference/runtime-components.md +0 -1366
- package/docs/reference/runtime-entities.md +0 -303
- package/docs/reference/runtime-modules.md +0 -2965
- package/docs/reference/sdk-api.md +0 -6698
- package/docs/roadmap.md +0 -79
- package/docs/work-amplification.md +0 -129
- package/runtime/ARCANE_RUNTIME_RELEASE.json +0 -826
package/src/app-descriptor.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {isDeepStrictEqual} from 'node:util';
|
|
2
|
-
import {
|
|
3
|
-
import {lstat,readFile} from 'node:fs/promises';
|
|
2
|
+
import {readFile} from 'node:fs/promises';
|
|
4
3
|
import path from 'node:path';
|
|
5
4
|
import {
|
|
6
5
|
ARCANE_MACHINE_BUNDLE_VERSION,
|
|
@@ -22,7 +21,7 @@ const METHOD_PATTERN=/^[a-z][a-z0-9]*(?:\.[a-z][a-zA-Z0-9]*)+$/u;
|
|
|
22
21
|
const FEATURE_PATTERN=/^[a-z][a-z0-9]*(?:[.-][a-z0-9]+)*$/u;
|
|
23
22
|
const PUBLISHER_PATTERN=/^[a-z0-9]+(?:-[a-z0-9]+)*$/u;
|
|
24
23
|
const NATIVE_TYPES=new Set(['app']);
|
|
25
|
-
const
|
|
24
|
+
const completeValue=value=>value;
|
|
26
25
|
const REGISTRY_PATH=path.join(
|
|
27
26
|
'machine_bundles',
|
|
28
27
|
'arcane-os-machine-bundle',
|
|
@@ -46,17 +45,17 @@ function assertOnlyKeys(value,allowed,label){
|
|
|
46
45
|
}
|
|
47
46
|
}
|
|
48
47
|
|
|
49
|
-
function safeText(value,label
|
|
50
|
-
if(typeof value!=='string'||value!==value.trim()||!value
|
|
48
|
+
function safeText(value,label){
|
|
49
|
+
if(typeof value!=='string'||value!==value.trim()||!value
|
|
51
50
|
||/[<>\u0000-\u001f\u007f]/u.test(value)){
|
|
52
|
-
fail(`${label} must be nonempty, trimmed
|
|
51
|
+
fail(`${label} must be nonempty, trimmed presentation text.`);
|
|
53
52
|
}
|
|
54
53
|
return value;
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
function uniqueSortedStrings(value,label,{pattern,
|
|
58
|
-
if(!Array.isArray(value)||(required&&value.length===0)
|
|
59
|
-
fail(`${label} must be ${required?'a nonempty':'an'} array
|
|
56
|
+
function uniqueSortedStrings(value,label,{pattern,required=false}={}){
|
|
57
|
+
if(!Array.isArray(value)||(required&&value.length===0)){
|
|
58
|
+
fail(`${label} must be ${required?'a nonempty':'an'} array.`);
|
|
60
59
|
}
|
|
61
60
|
const normalized=value.map((entry,index)=>{
|
|
62
61
|
if(typeof entry!=='string'||entry!==entry.trim()||!entry
|
|
@@ -65,68 +64,42 @@ function uniqueSortedStrings(value,label,{pattern,maximum=256,required=false}={}
|
|
|
65
64
|
}
|
|
66
65
|
return entry;
|
|
67
66
|
});
|
|
68
|
-
|
|
69
|
-
if(JSON.stringify(normalized)!==JSON.stringify([...normalized].sort())){
|
|
70
|
-
fail(`${label} must be sorted for deterministic projection.`);
|
|
71
|
-
}
|
|
72
|
-
return Object.freeze(normalized);
|
|
67
|
+
return completeValue(normalized);
|
|
73
68
|
}
|
|
74
69
|
|
|
75
70
|
function relativePaths(value,label,{required=false}={}){
|
|
76
|
-
if(!Array.isArray(value)||(required&&value.length===0)
|
|
77
|
-
fail(`${label} must be ${required?'a nonempty':'an'} array
|
|
71
|
+
if(!Array.isArray(value)||(required&&value.length===0)){
|
|
72
|
+
fail(`${label} must be ${required?'a nonempty':'an'} array.`);
|
|
78
73
|
}
|
|
79
74
|
const normalized=value.map((entry,index)=>normalizeRelativePath(entry,`${label}[${index}]`));
|
|
80
|
-
|
|
81
|
-
fail(`${label} must not contain duplicate paths.`);
|
|
82
|
-
}
|
|
83
|
-
return Object.freeze(normalized);
|
|
75
|
+
return completeValue(normalized);
|
|
84
76
|
}
|
|
85
77
|
|
|
86
|
-
function validateOrigins(value,label
|
|
87
|
-
if(!Array.isArray(value)
|
|
78
|
+
function validateOrigins(value,label){
|
|
79
|
+
if(!Array.isArray(value))fail(`${label} must be an array of origins.`);
|
|
88
80
|
const origins=value.map((origin,index)=>{
|
|
89
|
-
if(typeof origin!=='string'
|
|
90
|
-
|
|
91
|
-
let parsed;
|
|
92
|
-
try{parsed=new URL(origin);}
|
|
93
|
-
catch{fail(`${label}[${index}] is not a valid URL origin.`);}
|
|
94
|
-
if(parsed.origin!==origin||parsed.username||parsed.password
|
|
95
|
-
||parsed.pathname!=='/'||parsed.search||parsed.hash){
|
|
96
|
-
fail(`${label}[${index}] must be a canonical allowed origin.`);
|
|
97
|
-
}
|
|
98
|
-
if(parsed.protocol==='http:'){
|
|
99
|
-
if(!allowLoopbackHttp||!['127.0.0.1','[::1]'].includes(parsed.hostname)){
|
|
100
|
-
fail(`${label}[${index}] may use HTTP only for a numeric loopback host.`);
|
|
101
|
-
}
|
|
102
|
-
}else if(parsed.protocol!=='https:'){
|
|
103
|
-
fail(`${label}[${index}] must use HTTPS or an approved loopback HTTP origin.`);
|
|
104
|
-
}
|
|
105
|
-
return parsed.origin;
|
|
81
|
+
if(typeof origin!=='string'||!origin.trim())fail(`${label}[${index}] is invalid.`);
|
|
82
|
+
return origin;
|
|
106
83
|
});
|
|
107
|
-
|
|
108
|
-
if(JSON.stringify(origins)!==JSON.stringify([...origins].sort())){
|
|
109
|
-
fail(`${label} must be sorted for deterministic projection.`);
|
|
110
|
-
}
|
|
111
|
-
return Object.freeze(origins);
|
|
84
|
+
return completeValue(origins);
|
|
112
85
|
}
|
|
113
86
|
|
|
114
87
|
function validateLocalAIModelPolicy(value,label){
|
|
115
88
|
if(value===undefined)return undefined;
|
|
116
89
|
assertOnlyKeys(value,new Set(['verified_only','models']),label);
|
|
117
|
-
if(typeof value.verified_only!=='boolean'||!Array.isArray(value.models)
|
|
90
|
+
if(typeof value.verified_only!=='boolean'||!Array.isArray(value.models)){
|
|
118
91
|
fail(`${label} is invalid.`);
|
|
119
92
|
}
|
|
120
93
|
const models=value.models.map((model,index)=>{
|
|
121
94
|
const itemLabel=`${label}.models[${index}]`;
|
|
122
95
|
assertOnlyKeys(model,new Set(['name','definition']),itemLabel);
|
|
123
|
-
if(typeof model.name!=='string'||!model.name
|
|
96
|
+
if(typeof model.name!=='string'||!model.name
|
|
124
97
|
||typeof model.definition!=='string'||!model.definition.endsWith('Modelfile')){
|
|
125
98
|
fail(`${itemLabel} is invalid.`);
|
|
126
99
|
}
|
|
127
|
-
return
|
|
100
|
+
return completeValue({name:model.name,definition:normalizeRelativePath(model.definition,`${itemLabel}.definition`)});
|
|
128
101
|
});
|
|
129
|
-
return
|
|
102
|
+
return completeValue({verified_only:value.verified_only,models:completeValue(models)});
|
|
130
103
|
}
|
|
131
104
|
|
|
132
105
|
function validatePackage(value,appId){
|
|
@@ -142,7 +115,6 @@ function validatePackage(value,appId){
|
|
|
142
115
|
}
|
|
143
116
|
const shared=uniqueSortedStrings(value.shared,'descriptor.package.shared',{
|
|
144
117
|
pattern:/^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/u,
|
|
145
|
-
maximum:64,
|
|
146
118
|
required:true
|
|
147
119
|
});
|
|
148
120
|
let adapter;
|
|
@@ -154,7 +126,7 @@ function validatePackage(value,appId){
|
|
|
154
126
|
}else if(value.adapter!==undefined){
|
|
155
127
|
fail('descriptor.package.adapter is only valid for adapter packages.');
|
|
156
128
|
}
|
|
157
|
-
return
|
|
129
|
+
return completeValue({
|
|
158
130
|
entry,
|
|
159
131
|
strategy:value.strategy,
|
|
160
132
|
include,
|
|
@@ -175,45 +147,29 @@ function validatePublisher(value){
|
|
|
175
147
|
if(typeof value.id!=='string'||!PUBLISHER_PATTERN.test(value.id)){
|
|
176
148
|
fail('descriptor.publisher.id must be a lowercase publisher identifier.');
|
|
177
149
|
}
|
|
178
|
-
return
|
|
150
|
+
return completeValue({id:value.id,name:safeText(value.name,'descriptor.publisher.name')});
|
|
179
151
|
}
|
|
180
152
|
|
|
181
|
-
function validatePermissions(value){
|
|
153
|
+
function validatePermissions(value={}){
|
|
182
154
|
assertOnlyKeys(value,new Set(['capabilities','methods']),'descriptor.permissions');
|
|
183
|
-
const capabilities=uniqueSortedStrings(value.capabilities,'descriptor.permissions.capabilities',{
|
|
184
|
-
pattern:CAPABILITY_PATTERN
|
|
185
|
-
maximum:256
|
|
155
|
+
const capabilities=uniqueSortedStrings(value.capabilities??[],'descriptor.permissions.capabilities',{
|
|
156
|
+
pattern:CAPABILITY_PATTERN
|
|
186
157
|
});
|
|
187
|
-
|
|
188
|
-
fail('descriptor.permissions.capabilities must not combine external.open with web.embed.');
|
|
189
|
-
}
|
|
190
|
-
return Object.freeze({
|
|
158
|
+
return completeValue({
|
|
191
159
|
capabilities,
|
|
192
|
-
methods:uniqueSortedStrings(value.methods,'descriptor.permissions.methods',{
|
|
193
|
-
pattern:METHOD_PATTERN
|
|
194
|
-
maximum:512
|
|
160
|
+
methods:uniqueSortedStrings(value.methods??[],'descriptor.permissions.methods',{
|
|
161
|
+
pattern:METHOD_PATTERN
|
|
195
162
|
})
|
|
196
163
|
});
|
|
197
164
|
}
|
|
198
165
|
|
|
199
|
-
function validateSecurity(value
|
|
166
|
+
function validateSecurity(value={}){
|
|
200
167
|
assertOnlyKeys(value,new Set(['connectOrigins','frameOrigins','mediaOrigins']),'descriptor.security');
|
|
201
|
-
const frameOrigins=validateOrigins(
|
|
202
|
-
|
|
203
|
-
'descriptor.security.
|
|
204
|
-
{allowHttpsScheme:appId==='browser'}
|
|
205
|
-
);
|
|
206
|
-
if(frameOrigins.length&&!capabilities.includes('web.embed')){
|
|
207
|
-
fail('descriptor.security.frameOrigins requires the web.embed capability.');
|
|
208
|
-
}
|
|
209
|
-
return Object.freeze({
|
|
210
|
-
connectOrigins:validateOrigins(
|
|
211
|
-
value.connectOrigins,
|
|
212
|
-
'descriptor.security.connectOrigins',
|
|
213
|
-
{allowLoopbackHttp:true}
|
|
214
|
-
),
|
|
168
|
+
const frameOrigins=validateOrigins(value.frameOrigins??[],'descriptor.security.frameOrigins');
|
|
169
|
+
return completeValue({
|
|
170
|
+
connectOrigins:validateOrigins(value.connectOrigins??[],'descriptor.security.connectOrigins'),
|
|
215
171
|
frameOrigins,
|
|
216
|
-
mediaOrigins:validateOrigins(value.mediaOrigins,'descriptor.security.mediaOrigins')
|
|
172
|
+
mediaOrigins:validateOrigins(value.mediaOrigins??[],'descriptor.security.mediaOrigins')
|
|
217
173
|
});
|
|
218
174
|
}
|
|
219
175
|
|
|
@@ -226,7 +182,7 @@ function validateDocumentCatalog(value){
|
|
|
226
182
|
if(manifest.includes('/')||path.posix.extname(manifest).toLowerCase()!=='.json'){
|
|
227
183
|
fail(`${label}.manifest must be a JSON filename.`);
|
|
228
184
|
}
|
|
229
|
-
return
|
|
185
|
+
return completeValue({
|
|
230
186
|
policy:value.policy,
|
|
231
187
|
destination:normalizeRelativePath(value.destination,`${label}.destination`),
|
|
232
188
|
manifest
|
|
@@ -236,7 +192,7 @@ function validateDocumentCatalog(value){
|
|
|
236
192
|
'policy','release','destination','originals','manifest','expectedCount'
|
|
237
193
|
]),label);
|
|
238
194
|
if(value.policy!=='public-only'||!Number.isInteger(value.expectedCount)
|
|
239
|
-
||value.expectedCount<1
|
|
195
|
+
||value.expectedCount<1){
|
|
240
196
|
fail(`${label} has unsupported policy or expectedCount.`);
|
|
241
197
|
}
|
|
242
198
|
const destination=normalizeRelativePath(value.destination,`${label}.destination`);
|
|
@@ -249,7 +205,7 @@ function validateDocumentCatalog(value){
|
|
|
249
205
|
||originals.startsWith(`${destination}/`)){
|
|
250
206
|
fail(`${label}.destination and originals must not overlap.`);
|
|
251
207
|
}
|
|
252
|
-
return
|
|
208
|
+
return completeValue({
|
|
253
209
|
policy:value.policy,
|
|
254
210
|
release:normalizeRelativePath(value.release,`${label}.release`),
|
|
255
211
|
destination,
|
|
@@ -263,19 +219,14 @@ function validateNative(value,appId){
|
|
|
263
219
|
assertOnlyKeys(value,new Set(['type','icon','order','bundledApps','documentCatalog']),'descriptor.native');
|
|
264
220
|
if(!NATIVE_TYPES.has(value.type))fail('descriptor.native.type is unsupported.');
|
|
265
221
|
const icon=value.icon===null?null:normalizeRelativePath(value.icon,'descriptor.native.icon');
|
|
266
|
-
if(
|
|
267
|
-
fail('descriptor.native.
|
|
268
|
-
}
|
|
269
|
-
if(!Number.isInteger(value.order)||value.order<0||value.order>10000){
|
|
270
|
-
fail('descriptor.native.order must be an integer from 0 through 10000.');
|
|
222
|
+
if(!Number.isInteger(value.order)||value.order<0){
|
|
223
|
+
fail('descriptor.native.order must be a nonnegative integer.');
|
|
271
224
|
}
|
|
272
225
|
const bundledApps=uniqueSortedStrings(value.bundledApps,'descriptor.native.bundledApps',{
|
|
273
|
-
pattern:APP_ID_PATTERN
|
|
274
|
-
maximum:16
|
|
226
|
+
pattern:APP_ID_PATTERN
|
|
275
227
|
});
|
|
276
|
-
if(bundledApps.includes(appId))fail('descriptor.native.bundledApps must not include the app itself.');
|
|
277
228
|
const documentCatalog=validateDocumentCatalog(value.documentCatalog);
|
|
278
|
-
return
|
|
229
|
+
return completeValue({
|
|
279
230
|
type:value.type,
|
|
280
231
|
icon,
|
|
281
232
|
order:value.order,
|
|
@@ -284,16 +235,19 @@ function validateNative(value,appId){
|
|
|
284
235
|
});
|
|
285
236
|
}
|
|
286
237
|
|
|
287
|
-
function validateRequirements(value){
|
|
238
|
+
function validateRequirements(value,{targets}){
|
|
288
239
|
assertOnlyKeys(value,new Set(['arcaneProtocol','minimumCoreVersion','features']),'descriptor.requirements');
|
|
289
240
|
if(value.arcaneProtocol!==ARCANE_PROTOCOL)fail(`descriptor.requirements.arcaneProtocol must be ${ARCANE_PROTOCOL}.`);
|
|
290
|
-
|
|
291
|
-
|
|
241
|
+
const needsCore=targets.some(target=>target!=='browser');
|
|
242
|
+
if(needsCore&&value.minimumCoreVersion===undefined){
|
|
243
|
+
fail('descriptor.requirements.minimumCoreVersion is required for non-browser targets.');
|
|
244
|
+
}
|
|
245
|
+
if(value.minimumCoreVersion!==undefined)parseSemver(value.minimumCoreVersion);
|
|
246
|
+
return completeValue({
|
|
292
247
|
arcaneProtocol:value.arcaneProtocol,
|
|
293
|
-
minimumCoreVersion:value.minimumCoreVersion,
|
|
248
|
+
...(value.minimumCoreVersion===undefined?{}:{minimumCoreVersion:value.minimumCoreVersion}),
|
|
294
249
|
features:uniqueSortedStrings(value.features,'descriptor.requirements.features',{
|
|
295
|
-
pattern:FEATURE_PATTERN
|
|
296
|
-
maximum:128
|
|
250
|
+
pattern:FEATURE_PATTERN
|
|
297
251
|
})
|
|
298
252
|
});
|
|
299
253
|
}
|
|
@@ -318,12 +272,9 @@ export function validateAppDescriptor(value,{appId}={}){
|
|
|
318
272
|
if(targets.some(target=>!TARGET_IDS.includes(target)))fail('descriptor.targets contains an unknown SDK target.');
|
|
319
273
|
const packageDescriptor=validatePackage(value.package,value.id);
|
|
320
274
|
const native=validateNative(value.native,value.id);
|
|
321
|
-
const displayName=safeText(value.displayName,'descriptor.displayName'
|
|
275
|
+
const displayName=safeText(value.displayName,'descriptor.displayName');
|
|
322
276
|
const description=safeText(value.description,'descriptor.description');
|
|
323
277
|
if(targets.some(target=>target!=='browser')){
|
|
324
|
-
if(displayName.length>80||description.length>240){
|
|
325
|
-
fail('Native app displayName and description must fit the Arcane presentation limits.');
|
|
326
|
-
}
|
|
327
278
|
if(!native.icon){
|
|
328
279
|
fail('descriptor.native.icon is required when a native target is declared.');
|
|
329
280
|
}
|
|
@@ -333,8 +284,8 @@ export function validateAppDescriptor(value,{appId}={}){
|
|
|
333
284
|
fail('descriptor.native.icon must be covered by descriptor.package.include.');
|
|
334
285
|
}
|
|
335
286
|
}
|
|
336
|
-
const permissions=validatePermissions(value.permissions);
|
|
337
|
-
return
|
|
287
|
+
const permissions=validatePermissions(value.permissions??{});
|
|
288
|
+
return completeValue({
|
|
338
289
|
schemaVersion:APP_DESCRIPTOR_SCHEMA_VERSION,
|
|
339
290
|
id:value.id,
|
|
340
291
|
displayName,
|
|
@@ -343,14 +294,15 @@ export function validateAppDescriptor(value,{appId}={}){
|
|
|
343
294
|
publisher:validatePublisher(value.publisher),
|
|
344
295
|
package:packageDescriptor,
|
|
345
296
|
permissions,
|
|
346
|
-
security:validateSecurity(value.security
|
|
297
|
+
security:validateSecurity(value.security??{}),
|
|
347
298
|
native,
|
|
348
|
-
requirements:validateRequirements(value.requirements),
|
|
299
|
+
requirements:validateRequirements(value.requirements,{targets}),
|
|
349
300
|
targets
|
|
350
301
|
});
|
|
351
302
|
}
|
|
352
303
|
|
|
353
304
|
export function projectPackageManifest(descriptor){
|
|
305
|
+
const hasAuthoredSecurity=descriptor?.security!==undefined;
|
|
354
306
|
const value=validateAppDescriptor(descriptor,{appId:descriptor?.id});
|
|
355
307
|
const projection={
|
|
356
308
|
schemaVersion:1,
|
|
@@ -359,11 +311,11 @@ export function projectPackageManifest(descriptor){
|
|
|
359
311
|
version:value.version,
|
|
360
312
|
entry:value.package.entry,
|
|
361
313
|
strategy:value.package.strategy,
|
|
362
|
-
security:{
|
|
314
|
+
...(hasAuthoredSecurity?{security:{
|
|
363
315
|
connectOrigins:[...value.security.connectOrigins],
|
|
364
316
|
frameOrigins:[...value.security.frameOrigins],
|
|
365
317
|
mediaOrigins:[...value.security.mediaOrigins]
|
|
366
|
-
},
|
|
318
|
+
}}:{}),
|
|
367
319
|
...(value.package.localAIModelPolicy?{localAIModelPolicy:value.package.localAIModelPolicy}:{}),
|
|
368
320
|
include:[...value.package.include],
|
|
369
321
|
exclude:[...value.package.exclude],
|
|
@@ -371,23 +323,20 @@ export function projectPackageManifest(descriptor){
|
|
|
371
323
|
...(value.package.adapter?{adapter:value.package.adapter}:{})
|
|
372
324
|
};
|
|
373
325
|
const sharedPayloads=Object.fromEntries(
|
|
374
|
-
value.package.shared.map(id=>[id,
|
|
326
|
+
value.package.shared.map(id=>[id,completeValue([])])
|
|
375
327
|
);
|
|
376
328
|
validateAppPackageConfig(
|
|
377
329
|
projection,
|
|
378
330
|
value.id,
|
|
379
|
-
{sharedPayloads:
|
|
331
|
+
{sharedPayloads:completeValue(sharedPayloads)},
|
|
380
332
|
`${APP_DESCRIPTOR_NAME} projection`
|
|
381
333
|
);
|
|
382
334
|
return projection;
|
|
383
335
|
}
|
|
384
336
|
|
|
385
|
-
export function appDescriptorSha256(descriptor){
|
|
386
|
-
const value=validateAppDescriptor(descriptor,{appId:descriptor?.id});
|
|
387
|
-
return createHash('sha256').update(JSON.stringify(value)).digest('hex');
|
|
388
|
-
}
|
|
389
|
-
|
|
390
337
|
export function projectNativeDescriptor(descriptor,{source}={}){
|
|
338
|
+
const hasAuthoredPermissions=descriptor?.permissions!==undefined;
|
|
339
|
+
const hasAuthoredSecurity=descriptor?.security!==undefined;
|
|
391
340
|
const value=validateAppDescriptor(descriptor,{appId:descriptor?.id});
|
|
392
341
|
if(!value.targets.some(target=>target!=='browser')){
|
|
393
342
|
fail('A browser-only descriptor cannot be projected as a native Arcane app.');
|
|
@@ -400,12 +349,12 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
400
349
|
type:value.native.type,
|
|
401
350
|
source:source??`apps/${value.id}`,
|
|
402
351
|
entry:value.package.entry,
|
|
403
|
-
capabilities:[...value.permissions.capabilities],
|
|
404
|
-
security:{
|
|
352
|
+
...(hasAuthoredPermissions?{capabilities:[...value.permissions.capabilities]}:{}),
|
|
353
|
+
...(hasAuthoredSecurity?{security:{
|
|
405
354
|
connectOrigins:[...value.security.connectOrigins],
|
|
406
355
|
frameOrigins:[...value.security.frameOrigins],
|
|
407
356
|
mediaOrigins:[...value.security.mediaOrigins]
|
|
408
|
-
},
|
|
357
|
+
}}:{}),
|
|
409
358
|
...(value.native.bundledApps.length?{bundledApps:[...value.native.bundledApps]}:{}),
|
|
410
359
|
...(value.native.documentCatalog?{documentCatalog:value.native.documentCatalog}:{}),
|
|
411
360
|
include:[...value.package.include]
|
|
@@ -414,10 +363,6 @@ export function projectNativeDescriptor(descriptor,{source}={}){
|
|
|
414
363
|
|
|
415
364
|
function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
416
365
|
const native=nativeDescriptor??{};
|
|
417
|
-
if(packageManifest.security!==undefined&&native.security!==undefined
|
|
418
|
-
&&!isDeepStrictEqual(packageManifest.security,native.security)){
|
|
419
|
-
fail('Legacy package and native registry security policies do not match.');
|
|
420
|
-
}
|
|
421
366
|
const security=packageManifest.security??native.security??{};
|
|
422
367
|
return validateAppDescriptor({
|
|
423
368
|
schemaVersion:APP_DESCRIPTOR_SCHEMA_VERSION,
|
|
@@ -432,28 +377,28 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
432
377
|
...(packageManifest.localAIModelPolicy?{localAIModelPolicy:packageManifest.localAIModelPolicy}:{}),
|
|
433
378
|
include:[...packageManifest.include],
|
|
434
379
|
exclude:[...(packageManifest.exclude??[])],
|
|
435
|
-
shared:[...packageManifest.shared]
|
|
380
|
+
shared:[...packageManifest.shared],
|
|
436
381
|
...(packageManifest.adapter?{adapter:packageManifest.adapter}:{})
|
|
437
382
|
},
|
|
438
383
|
permissions:{
|
|
439
|
-
capabilities:[...(native.capabilities??[])]
|
|
384
|
+
capabilities:[...(native.capabilities??[])],
|
|
440
385
|
methods:[]
|
|
441
386
|
},
|
|
442
387
|
security:{
|
|
443
|
-
connectOrigins:[...(security.connectOrigins??[])]
|
|
444
|
-
frameOrigins:[...(security.frameOrigins??[])]
|
|
445
|
-
mediaOrigins:[...(security.mediaOrigins??[])]
|
|
388
|
+
connectOrigins:[...(security.connectOrigins??[])],
|
|
389
|
+
frameOrigins:[...(security.frameOrigins??[])],
|
|
390
|
+
mediaOrigins:[...(security.mediaOrigins??[])]
|
|
446
391
|
},
|
|
447
392
|
native:{
|
|
448
393
|
type:native.type??'app',
|
|
449
394
|
icon:native.icon??null,
|
|
450
395
|
order:Number.isInteger(native.order)?native.order:100,
|
|
451
|
-
bundledApps:[...(native.bundledApps??[])]
|
|
396
|
+
bundledApps:[...(native.bundledApps??[])],
|
|
452
397
|
...(native.documentCatalog?{documentCatalog:native.documentCatalog}:{})
|
|
453
398
|
},
|
|
454
399
|
requirements:{
|
|
455
400
|
arcaneProtocol:ARCANE_PROTOCOL,
|
|
456
|
-
minimumCoreVersion:ARCANE_MACHINE_BUNDLE_VERSION,
|
|
401
|
+
...(nativeDescriptor===undefined?{}:{minimumCoreVersion:ARCANE_MACHINE_BUNDLE_VERSION}),
|
|
457
402
|
features:[]
|
|
458
403
|
},
|
|
459
404
|
targets:nativeDescriptor!==undefined
|
|
@@ -464,8 +409,6 @@ function synthesizedDescriptor(packageManifest,nativeDescriptor){
|
|
|
464
409
|
|
|
465
410
|
async function readJsonFile(filePath,label,{optional=false}={}){
|
|
466
411
|
try{
|
|
467
|
-
const info=await lstat(filePath);
|
|
468
|
-
if(info.isSymbolicLink()||!info.isFile())fail(`${label} must be a real file.`);
|
|
469
412
|
return JSON.parse(await readFile(filePath,'utf8'));
|
|
470
413
|
}catch(error){
|
|
471
414
|
if(optional&&error?.code==='ENOENT')return null;
|
|
@@ -479,11 +422,11 @@ export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageMani
|
|
|
479
422
|
const authored=await readJsonFile(descriptorPath,`apps/${appId}/${APP_DESCRIPTOR_NAME}`,{optional:true});
|
|
480
423
|
if(authored){
|
|
481
424
|
const descriptor=validateAppDescriptor(authored,{appId});
|
|
482
|
-
const projection=projectPackageManifest(
|
|
425
|
+
const projection=projectPackageManifest(authored);
|
|
483
426
|
if(!isDeepStrictEqual(projection,packageManifest)){
|
|
484
427
|
fail(`${APP_DESCRIPTOR_NAME} does not project exactly to arcane-package.json.`);
|
|
485
428
|
}
|
|
486
|
-
return
|
|
429
|
+
return completeValue({descriptor,source:'authored',descriptorPath});
|
|
487
430
|
}
|
|
488
431
|
|
|
489
432
|
const registry=await readJsonFile(
|
|
@@ -492,7 +435,7 @@ export async function loadAppDescriptor({workspaceRoot,appRoot,appId,packageMani
|
|
|
492
435
|
{optional:true}
|
|
493
436
|
);
|
|
494
437
|
const nativeDescriptor=isObject(registry?.apps?.[appId])?registry.apps[appId]:undefined;
|
|
495
|
-
return
|
|
438
|
+
return completeValue({
|
|
496
439
|
descriptor:synthesizedDescriptor(packageManifest,nativeDescriptor),
|
|
497
440
|
source:nativeDescriptor?'legacy-registry':'legacy-package',
|
|
498
441
|
descriptorPath:null
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import {lstat,readdir,realpath} from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import {fileURLToPath} from 'node:url';
|
|
4
|
+
import {ArcaneError,ERROR_CODES,throwIfAborted} from './errors.mjs';
|
|
5
|
+
import {runProcess} from './process.mjs';
|
|
6
|
+
|
|
7
|
+
const TEST_RUNNER_PATH=fileURLToPath(new URL('../bin/arcane-test.mjs',import.meta.url));
|
|
8
|
+
|
|
9
|
+
function normalizedTestOutput(value){
|
|
10
|
+
return typeof value==='string'?value:'';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function captureFailedTest(testFile,result,workspaceRoot){
|
|
14
|
+
return {
|
|
15
|
+
testFile:path.relative(workspaceRoot,testFile).replaceAll('\\','/'),
|
|
16
|
+
exitCode:result.code,
|
|
17
|
+
signal:result.signal,
|
|
18
|
+
stdout:normalizedTestOutput(result.stdout),
|
|
19
|
+
stderr:normalizedTestOutput(result.stderr)
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function failedTestMessage(failures){
|
|
24
|
+
const summary=`${String(failures.length)} isolated test file${failures.length===1?'':'s'} failed.`;
|
|
25
|
+
let diagnostics='';
|
|
26
|
+
for(const failure of failures){
|
|
27
|
+
if(failure.stdout){
|
|
28
|
+
diagnostics+=`\n\n${failure.testFile} stdout:\n${failure.stdout}`;
|
|
29
|
+
}
|
|
30
|
+
if(failure.stderr){
|
|
31
|
+
diagnostics+=`\n\n${failure.testFile} stderr:\n${failure.stderr}`;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return `${summary}${diagnostics}`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function samePath(left,right){
|
|
38
|
+
const a=path.resolve(left);
|
|
39
|
+
const b=path.resolve(right);
|
|
40
|
+
return process.platform==='win32'?a.toLowerCase()===b.toLowerCase():a===b;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function pathInside(root,candidate){
|
|
44
|
+
const relative=path.relative(root,candidate);
|
|
45
|
+
return relative===''||(!relative.startsWith(`..${path.sep}`)
|
|
46
|
+
&&relative!=='..'&&!path.isAbsolute(relative));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function collectTests(root,testCodeRoot,files,signal){
|
|
50
|
+
throwIfAborted(signal);
|
|
51
|
+
const before=await lstat(root);
|
|
52
|
+
if(before.isSymbolicLink()||!before.isDirectory()){
|
|
53
|
+
throw new ArcaneError(
|
|
54
|
+
ERROR_CODES.operationFailed,
|
|
55
|
+
`Selected application test path is not a real directory: ${root}.`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
const canonical=await realpath(root);
|
|
59
|
+
if(!samePath(root,canonical)||!pathInside(testCodeRoot,canonical)){
|
|
60
|
+
throw new ArcaneError(
|
|
61
|
+
ERROR_CODES.operationFailed,
|
|
62
|
+
`Selected application test directory leaves its test-code directory: ${root}.`
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
throwIfAborted(signal);
|
|
66
|
+
const entries=await readdir(root,{withFileTypes:true});
|
|
67
|
+
for(const entry of entries.sort((left,right)=>left.name.localeCompare(right.name,'en'))){
|
|
68
|
+
throwIfAborted(signal);
|
|
69
|
+
const absolute=path.join(root,entry.name);
|
|
70
|
+
const info=await lstat(absolute);
|
|
71
|
+
if(info.isSymbolicLink()){
|
|
72
|
+
throw new ArcaneError(
|
|
73
|
+
ERROR_CODES.operationFailed,
|
|
74
|
+
`Selected application tests refuse symbolic links: ${absolute}.`
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
if(info.isDirectory()){
|
|
78
|
+
await collectTests(absolute,testCodeRoot,files,signal);
|
|
79
|
+
}else if(info.isFile()&&/\.test\.(?:mjs|cjs|js)$/u.test(entry.name)){
|
|
80
|
+
const canonicalFile=await realpath(absolute);
|
|
81
|
+
if(!samePath(absolute,canonicalFile)||!pathInside(testCodeRoot,canonicalFile)){
|
|
82
|
+
throw new ArcaneError(
|
|
83
|
+
ERROR_CODES.operationFailed,
|
|
84
|
+
`Selected application test file leaves its test-code directory: ${absolute}.`
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
files.add(canonicalFile);
|
|
88
|
+
}else if(!info.isFile()){
|
|
89
|
+
throw new ArcaneError(
|
|
90
|
+
ERROR_CODES.operationFailed,
|
|
91
|
+
`Selected application test path is not a regular file or directory: ${absolute}.`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
throwIfAborted(signal);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function collectOptionalTests(root,applicationRoot,files,signal){
|
|
99
|
+
throwIfAborted(signal);
|
|
100
|
+
try{await lstat(root);}
|
|
101
|
+
catch(error){
|
|
102
|
+
if(error?.code==='ENOENT')return;
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
const canonicalApplicationRoot=await realpath(applicationRoot);
|
|
106
|
+
const canonicalTestRoot=await realpath(root);
|
|
107
|
+
if(!samePath(applicationRoot,canonicalApplicationRoot)
|
|
108
|
+
||!samePath(root,canonicalTestRoot)
|
|
109
|
+
||!pathInside(canonicalApplicationRoot,canonicalTestRoot)){
|
|
110
|
+
throw new ArcaneError(
|
|
111
|
+
ERROR_CODES.operationFailed,
|
|
112
|
+
`Selected application test-code directory leaves its application: ${root}.`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
await collectTests(canonicalTestRoot,canonicalTestRoot,files,signal);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function selectedTestFiles({workspaceRoot,workspaceMode,appRoot,signal}){
|
|
119
|
+
const files=new Set();
|
|
120
|
+
if(workspaceMode==='external'){
|
|
121
|
+
await collectOptionalTests(path.join(workspaceRoot,'test'),workspaceRoot,files,signal);
|
|
122
|
+
}
|
|
123
|
+
if(appRoot)await collectOptionalTests(path.join(appRoot,'test'),appRoot,files,signal);
|
|
124
|
+
return [...files].sort();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function runApplicationTests({
|
|
128
|
+
workspaceRoot,
|
|
129
|
+
workspaceMode='packager',
|
|
130
|
+
appId,
|
|
131
|
+
appRoot,
|
|
132
|
+
signal,
|
|
133
|
+
onEvent
|
|
134
|
+
}={}){
|
|
135
|
+
throwIfAborted(signal);
|
|
136
|
+
const workspace={workspaceRoot,workspaceMode,appId,appRoot};
|
|
137
|
+
const selection={workspaceRoot,workspaceMode,appRoot,signal};
|
|
138
|
+
const testFiles=await selectedTestFiles(selection);
|
|
139
|
+
if(testFiles.length===0){
|
|
140
|
+
await onEvent?.({
|
|
141
|
+
type:'test.skipped',
|
|
142
|
+
message:'No JavaScript test files were found.',
|
|
143
|
+
data:{workspaceRoot,appId}
|
|
144
|
+
});
|
|
145
|
+
throwIfAborted(signal);
|
|
146
|
+
return {
|
|
147
|
+
...workspace,
|
|
148
|
+
passed:true,
|
|
149
|
+
skipped:true,
|
|
150
|
+
testFiles:[],
|
|
151
|
+
output:''
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const outputs=[];
|
|
155
|
+
const failures=[];
|
|
156
|
+
for(const testFile of testFiles){
|
|
157
|
+
throwIfAborted(signal);
|
|
158
|
+
const result=await runProcess(
|
|
159
|
+
process.execPath,
|
|
160
|
+
[TEST_RUNNER_PATH,testFile],
|
|
161
|
+
{
|
|
162
|
+
cwd:workspaceRoot,
|
|
163
|
+
signal,
|
|
164
|
+
onEvent,
|
|
165
|
+
allowNonzero:true
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
if(result.code===0){
|
|
169
|
+
if(result.stdout)outputs.push(result.stdout);
|
|
170
|
+
}else if(result.code===1){
|
|
171
|
+
failures.push(captureFailedTest(testFile,result,workspaceRoot));
|
|
172
|
+
}else{
|
|
173
|
+
throw new ArcaneError(
|
|
174
|
+
ERROR_CODES.operationFailed,
|
|
175
|
+
`The isolated test runner exited with code ${String(result.code)} for ${path.basename(testFile)}.`,
|
|
176
|
+
{details:{testFile,result}}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if(failures.length>0){
|
|
181
|
+
throw new ArcaneError(
|
|
182
|
+
ERROR_CODES.operationFailed,
|
|
183
|
+
failedTestMessage(failures),
|
|
184
|
+
{
|
|
185
|
+
details:{
|
|
186
|
+
testFiles:failures.map(failure=>failure.testFile),
|
|
187
|
+
failures
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
throwIfAborted(signal);
|
|
193
|
+
return {
|
|
194
|
+
...workspace,
|
|
195
|
+
passed:true,
|
|
196
|
+
skipped:false,
|
|
197
|
+
testFiles:testFiles.map(file=>path.relative(workspaceRoot,file).replaceAll('\\','/')),
|
|
198
|
+
output:outputs.join('')
|
|
199
|
+
};
|
|
200
|
+
}
|