@markii/host 0.0.1 → 0.17.0-next.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.
- package/README.md +20 -2
- package/dist/browser-worker.js +639 -0
- package/dist/browser.d.ts +61 -0
- package/dist/browser.js +172 -0
- package/dist/conformance/host/01-plain-open/expected.json +5 -0
- package/dist/conformance/host/01-plain-open/note.mk.md +8 -0
- package/dist/conformance/host/01-plain-open/scenario.json +6 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/expected.json +16 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/note.mk.md +5 -0
- package/dist/conformance/host/02-run-manual-grant-accepted/scenario.json +14 -0
- package/dist/conformance/host/03-run-manual-grant-refused/expected.json +18 -0
- package/dist/conformance/host/03-run-manual-grant-refused/note.mk.md +5 -0
- package/dist/conformance/host/03-run-manual-grant-refused/scenario.json +14 -0
- package/dist/conformance/host/04-run-auto-write-denied/expected.json +16 -0
- package/dist/conformance/host/04-run-auto-write-denied/note.mk.md +10 -0
- package/dist/conformance/host/04-run-auto-write-denied/scenario.json +13 -0
- package/dist/conformance/host/05-export-html-pack-directive/expected.json +14 -0
- package/dist/conformance/host/05-export-html-pack-directive/note.mk.md +8 -0
- package/dist/conformance/host/05-export-html-pack-directive/scenario.json +6 -0
- package/dist/conformance/host/06-install-pack-then-render/expected.json +18 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/06-install-pack-then-render/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/06-install-pack-then-render/note.mk.md +5 -0
- package/dist/conformance/host/06-install-pack-then-render/scenario.json +15 -0
- package/dist/conformance/host/07-complete-in-directive/expected.json +14 -0
- package/dist/conformance/host/07-complete-in-directive/note.mk.md +3 -0
- package/dist/conformance/host/07-complete-in-directive/scenario.json +6 -0
- package/dist/conformance/host/08-hover-standard-component/expected.json +5 -0
- package/dist/conformance/host/08-hover-standard-component/note.mk.md +5 -0
- package/dist/conformance/host/08-hover-standard-component/scenario.json +6 -0
- package/dist/conformance/host/09-insert-skeleton/expected.json +5 -0
- package/dist/conformance/host/09-insert-skeleton/note.mk.md +4 -0
- package/dist/conformance/host/09-insert-skeleton/scenario.json +6 -0
- package/dist/conformance/host/10-scripts-disabled/expected.json +11 -0
- package/dist/conformance/host/10-scripts-disabled/note.mk.md +5 -0
- package/dist/conformance/host/10-scripts-disabled/scenario.json +9 -0
- package/dist/conformance/host/11-complete-closing-fence/expected.json +5 -0
- package/dist/conformance/host/11-complete-closing-fence/note.mk.md +5 -0
- package/dist/conformance/host/11-complete-closing-fence/scenario.json +12 -0
- package/dist/conformance/host/12-complete-nested-container/expected.json +5 -0
- package/dist/conformance/host/12-complete-nested-container/note.mk.md +5 -0
- package/dist/conformance/host/12-complete-nested-container/scenario.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/expected.json +12 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/13-install-pack-declined/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/13-install-pack-declined/note.mk.md +5 -0
- package/dist/conformance/host/13-install-pack-declined/scenario.json +10 -0
- package/dist/conformance/host/14-remove-pack/expected.json +14 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/pack.json +13 -0
- package/dist/conformance/host/14-remove-pack/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/14-remove-pack/note.mk.md +5 -0
- package/dist/conformance/host/14-remove-pack/scenario.json +11 -0
- package/dist/conformance/host/15-load-packs-engine-views/expected.json +19 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/pack.json +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/files/duo-1.0.0/webview.js +1 -0
- package/dist/conformance/host/15-load-packs-engine-views/note.mk.md +9 -0
- package/dist/conformance/host/15-load-packs-engine-views/scenario.json +10 -0
- package/dist/conformance/host/16-pack-trust/expected.json +37 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/ana-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/pack.json +12 -0
- package/dist/conformance/host/16-pack-trust/files/sneaky-1.0.0/webview.js +1 -0
- package/dist/conformance/host/16-pack-trust/note.mk.md +5 -0
- package/dist/conformance/host/16-pack-trust/scenario.json +26 -0
- package/dist/conformance/host/README.md +221 -0
- package/dist/conformance.d.ts +2 -0
- package/dist/conformance.js +5 -0
- package/dist/diagnostics/render-diagnostics.d.ts +41 -0
- package/dist/diagnostics/render-diagnostics.js +46 -0
- package/dist/export/cascade.d.ts +98 -0
- package/dist/export/cascade.js +159 -0
- package/dist/export/export-zip.d.ts +19 -0
- package/dist/export/export-zip.js +38 -0
- package/dist/export/image-embed.d.ts +126 -0
- package/dist/export/image-embed.js +274 -0
- package/dist/export/note-export.d.ts +332 -0
- package/dist/export/note-export.js +366 -0
- package/dist/export/note-links.d.ts +83 -0
- package/dist/export/note-links.js +241 -0
- package/dist/glue.wasm +0 -0
- package/dist/host/adapter.d.ts +218 -0
- package/dist/host/adapter.js +9 -0
- package/dist/host/cascade-export-behavior.d.ts +120 -0
- package/dist/host/cascade-export-behavior.js +151 -0
- package/dist/host/conformance-runner.d.ts +159 -0
- package/dist/host/conformance-runner.js +632 -0
- package/dist/host/create-host.d.ts +251 -0
- package/dist/host/create-host.js +329 -0
- package/dist/host/editor-behavior.d.ts +153 -0
- package/dist/host/editor-behavior.js +223 -0
- package/dist/host/export/md-plain.d.ts +15 -0
- package/dist/host/export/md-plain.js +125 -0
- package/dist/host/export-behavior.d.ts +111 -0
- package/dist/host/export-behavior.js +257 -0
- package/dist/host/labels.d.ts +23 -0
- package/dist/host/labels.js +32 -0
- package/dist/host/pack-archive.d.ts +30 -0
- package/dist/host/pack-archive.js +62 -0
- package/dist/host/pack-install.d.ts +87 -0
- package/dist/host/pack-install.js +186 -0
- package/dist/host/pack-load.d.ts +71 -0
- package/dist/host/pack-load.js +57 -0
- package/dist/host/pack-remove.d.ts +21 -0
- package/dist/host/pack-remove.js +65 -0
- package/dist/host/pack-trust-behavior.d.ts +79 -0
- package/dist/host/pack-trust-behavior.js +262 -0
- package/dist/host/refresh-interval.d.ts +37 -0
- package/dist/host/refresh-interval.js +59 -0
- package/dist/host/run-behavior.d.ts +88 -0
- package/dist/host/run-behavior.js +189 -0
- package/dist/host/run-diagnostics.d.ts +26 -0
- package/dist/host/run-diagnostics.js +43 -0
- package/dist/host/script-execution.d.ts +72 -0
- package/dist/host/script-execution.js +68 -0
- package/dist/host/terminal-packs.d.ts +39 -0
- package/dist/host/terminal-packs.js +51 -0
- package/dist/html-engine.d.ts +2 -0
- package/dist/html-engine.js +29 -0
- package/dist/index.d.ts +95 -0
- package/dist/index.js +69 -0
- package/dist/insert/component-catalog.d.ts +23 -0
- package/dist/insert/component-catalog.js +96 -0
- package/dist/node-fs.d.ts +14 -0
- package/dist/node-fs.js +70 -0
- package/dist/node-worker.js +20657 -0
- package/dist/node.d.ts +6 -0
- package/dist/node.js +13 -0
- package/dist/package.json +3 -0
- package/dist/packs/discover.d.ts +119 -0
- package/dist/packs/discover.js +266 -0
- package/dist/packs/pack-diagnostics.d.ts +79 -0
- package/dist/packs/pack-diagnostics.js +103 -0
- package/dist/packs/pack-paths.d.ts +53 -0
- package/dist/packs/pack-paths.js +105 -0
- package/dist/packs/pack-render-registry.d.ts +82 -0
- package/dist/packs/pack-render-registry.js +182 -0
- package/dist/packs/pack-scripts.d.ts +20 -0
- package/dist/packs/pack-scripts.js +96 -0
- package/dist/packs/pack-trust.d.ts +57 -0
- package/dist/packs/pack-trust.js +111 -0
- package/dist/packs/prebuilt.d.ts +49 -0
- package/dist/packs/prebuilt.js +97 -0
- package/dist/path-util.d.ts +24 -0
- package/dist/path-util.js +82 -0
- package/dist/run/browser-isolate.d.ts +45 -0
- package/dist/run/browser-isolate.js +85 -0
- package/dist/run/bundle-document.d.ts +29 -0
- package/dist/run/bundle-document.js +40 -0
- package/dist/run/bundle-run.d.ts +188 -0
- package/dist/run/bundle-run.js +232 -0
- package/dist/run/grant-flow.d.ts +232 -0
- package/dist/run/grant-flow.js +390 -0
- package/dist/run/ip-address.d.ts +41 -0
- package/dist/run/ip-address.js +255 -0
- package/dist/run/isolate.d.ts +59 -0
- package/dist/run/isolate.js +22 -0
- package/dist/run/lua-resolver.d.ts +21 -0
- package/dist/run/lua-resolver.js +63 -0
- package/dist/run/net-bridge-worker.d.ts +26 -0
- package/dist/run/net-bridge-worker.js +66 -0
- package/dist/run/net-bridge.d.ts +56 -0
- package/dist/run/net-bridge.js +59 -0
- package/dist/run/net-pinning.d.ts +82 -0
- package/dist/run/net-pinning.js +137 -0
- package/dist/run/net-provider.d.ts +72 -0
- package/dist/run/net-provider.js +303 -0
- package/dist/run/node-isolate.d.ts +22 -0
- package/dist/run/node-isolate.js +68 -0
- package/dist/run/run-flow.d.ts +186 -0
- package/dist/run/run-flow.js +323 -0
- package/dist/run/run-host.d.ts +107 -0
- package/dist/run/run-host.js +241 -0
- package/dist/run/run-job.d.ts +130 -0
- package/dist/run/run-job.js +279 -0
- package/dist/run/run-progress.d.ts +55 -0
- package/dist/run/run-progress.js +42 -0
- package/dist/run/run-trace.d.ts +44 -0
- package/dist/run/run-trace.js +36 -0
- package/dist/run/script-requirements.d.ts +98 -0
- package/dist/run/script-requirements.js +212 -0
- package/dist/run/snapshot-storage.d.ts +25 -0
- package/dist/run/snapshot-storage.js +31 -0
- package/dist/run/stale-values.d.ts +25 -0
- package/dist/run/stale-values.js +32 -0
- package/dist/run/worker-entry-browser.d.ts +62 -0
- package/dist/run/worker-entry-browser.js +172 -0
- package/dist/run/worker-entry.d.ts +1 -0
- package/dist/run/worker-entry.js +98 -0
- package/dist/values-failure.d.ts +17 -0
- package/dist/values-failure.js +1 -0
- package/dist/values-merge.d.ts +39 -0
- package/dist/values-merge.js +33 -0
- package/package.json +80 -4
|
@@ -0,0 +1,632 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Executes one `conformance/host/<scenario>` case against any real `HostAdapter`, so a single
|
|
3
|
+
* plain-data corpus proves the three apps' adapters behave the same.
|
|
4
|
+
*
|
|
5
|
+
* `conformance/host/` itself stays plain data (no TypeScript, matching the
|
|
6
|
+
* repo's existing corpora): a `note.mk.md`, a `scenario.json` (the ordered
|
|
7
|
+
* actions), an `expected.json` (the outcomes), and an optional `files/`
|
|
8
|
+
* directory. This module is the one place that reads that data, drives it
|
|
9
|
+
* through a `MarkiiHost`, and reports what happened.
|
|
10
|
+
*
|
|
11
|
+
* THE CAPABILITY RULE (the design's crux): `ACTION_CAPABILITY` is the one
|
|
12
|
+
* static map from an action's type to the capability it needs.
|
|
13
|
+
* `hostDeclaresCapability` checks the map against the ADAPTER UNDER TEST,
|
|
14
|
+
* never against the scenario data — the corpus itself never encodes
|
|
15
|
+
* per-host knowledge. `createMarkiiHost`'s own behaviors already return the
|
|
16
|
+
* shared `Unsupported` shape when a capability is missing; this module's
|
|
17
|
+
* job is to know that the fact should be a PASS (the correct thing for
|
|
18
|
+
* this host to say), not something to diff against `expected.json`.
|
|
19
|
+
*
|
|
20
|
+
* THE SCRIPTS-DISABLED SCENARIO (`10-scripts-disabled`) is not a
|
|
21
|
+
* `MarkiiHost` capability at all: the device-local execution switch is
|
|
22
|
+
* decided by each app's OWN call site (`preview-panel.ts`, `main.ts`)
|
|
23
|
+
* before it ever reaches `host.run`, using the shared, host-neutral
|
|
24
|
+
* wording in `./script-execution.js`. This module reproduces that same
|
|
25
|
+
* pre-check here, over the same shared functions and the SAME
|
|
26
|
+
* `adapter.labels` a real call site would use, so the scenario proves
|
|
27
|
+
* wording parity across hosts without inventing a capability the contract
|
|
28
|
+
* does not have. The scenario `10-scripts-disabled` holds the behavior.
|
|
29
|
+
*
|
|
30
|
+
* SIMULATED EXECUTION. A `run` action's `simulate` field (plain data) tells
|
|
31
|
+
* this module what the isolate would have produced, instead of spawning a
|
|
32
|
+
* real Lua worker: whether the requested value needed a specific granted
|
|
33
|
+
* host, and whether it was a write blocked under the read-only tier. Real
|
|
34
|
+
* sandbox enforcement is `conformance/executor/`'s job; this corpus proves
|
|
35
|
+
* the HOST-LEVEL plumbing around it (grant prompts, wording, capability
|
|
36
|
+
* declarations, trigger forwarding) — see this file's `simulatedRunResult`.
|
|
37
|
+
* Every OTHER action runs through the real, unmocked shared behavior code.
|
|
38
|
+
*/
|
|
39
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
40
|
+
import { dirname } from 'node:path';
|
|
41
|
+
import { fileURLToPath } from 'node:url';
|
|
42
|
+
import { createHtmlExportEngine } from '../html-engine.js';
|
|
43
|
+
import { createNodeArchiveExtractFs } from '../node-fs.js';
|
|
44
|
+
import { usesPackTrust } from './pack-trust-behavior.js';
|
|
45
|
+
import { join, relative, sep } from 'node:path';
|
|
46
|
+
import { zipSync } from 'fflate';
|
|
47
|
+
import { conformanceDir } from '@markii/core/corpus';
|
|
48
|
+
import { createMarkiiHost, } from './create-host.js';
|
|
49
|
+
import { scriptsDisabledDiagnosticLine, scriptsDisabledNotice, } from './script-execution.js';
|
|
50
|
+
// --- corpus location ----------------------------------------------------
|
|
51
|
+
/**
|
|
52
|
+
* `conformance/host/`, resolved relative to this module's own file, never
|
|
53
|
+
* `process.cwd()`. An installed package carries the corpus at
|
|
54
|
+
* `dist/conformance/host/`, a sibling of `dist/host/`; a repo checkout runs
|
|
55
|
+
* from `src/host/` and finds the repo-root corpus through
|
|
56
|
+
* `@markii/core/corpus`'s `conformanceDir`.
|
|
57
|
+
*/
|
|
58
|
+
export function hostConformanceDir() {
|
|
59
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
60
|
+
const packaged = join(here, '..', 'conformance', 'host');
|
|
61
|
+
if (existsSync(packaged))
|
|
62
|
+
return packaged;
|
|
63
|
+
return join(conformanceDir(), 'host');
|
|
64
|
+
}
|
|
65
|
+
/** Every scenario directory name under `dir`, sorted. */
|
|
66
|
+
export function listHostScenarioNames(dir = hostConformanceDir()) {
|
|
67
|
+
return readdirSync(dir, { withFileTypes: true })
|
|
68
|
+
.filter((entry) => entry.isDirectory())
|
|
69
|
+
.map((entry) => entry.name)
|
|
70
|
+
.sort();
|
|
71
|
+
}
|
|
72
|
+
/** Loads one scenario's three files. Never validates beyond `JSON.parse` — a malformed fixture is a corpus bug, and throwing loudly here is the right failure mode for it. */
|
|
73
|
+
export function loadHostScenario(name, baseDir = hostConformanceDir()) {
|
|
74
|
+
const dir = join(baseDir, name);
|
|
75
|
+
const noteText = readFileSync(join(dir, 'note.mk.md'), 'utf8');
|
|
76
|
+
const scenario = JSON.parse(readFileSync(join(dir, 'scenario.json'), 'utf8'));
|
|
77
|
+
const expected = JSON.parse(readFileSync(join(dir, 'expected.json'), 'utf8'));
|
|
78
|
+
return { dir, noteText, scenario, expected };
|
|
79
|
+
}
|
|
80
|
+
// --- .mkp archive zipping (from a plain files/ directory, never a committed binary) ---
|
|
81
|
+
function readDirRecursive(dir, base) {
|
|
82
|
+
const out = {};
|
|
83
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
84
|
+
const full = join(dir, entry.name);
|
|
85
|
+
if (entry.isDirectory()) {
|
|
86
|
+
Object.assign(out, readDirRecursive(full, base));
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const relativeName = relative(base, full).split(sep).join('/');
|
|
90
|
+
out[relativeName] = new Uint8Array(readFileSync(full));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
/** Zips a scenario's `files/<name>` directory into `.mkp` archive bytes on the fly — this is how the corpus ships pack fixtures as plain files instead of a committed binary. */
|
|
96
|
+
export function zipArchiveDirectory(archiveDir) {
|
|
97
|
+
return zipSync(readDirRecursive(archiveDir, archiveDir));
|
|
98
|
+
}
|
|
99
|
+
/** The one static map from an action's type to the capability it needs. `undefined` means every host supports it (there is nothing to decline). */
|
|
100
|
+
export const ACTION_CAPABILITY = {
|
|
101
|
+
open: undefined,
|
|
102
|
+
run: 'isolate',
|
|
103
|
+
exportHtml: 'export-format',
|
|
104
|
+
exportAnsi: 'export-format',
|
|
105
|
+
installPack: 'packs',
|
|
106
|
+
loadPacks: 'packs',
|
|
107
|
+
removePack: 'packs',
|
|
108
|
+
placePack: 'pack-trust',
|
|
109
|
+
listPacks: 'pack-trust',
|
|
110
|
+
enablePack: 'pack-trust',
|
|
111
|
+
disablePack: 'pack-trust',
|
|
112
|
+
completeAt: 'editor',
|
|
113
|
+
hoverAt: 'editor',
|
|
114
|
+
insertComponent: 'editor',
|
|
115
|
+
};
|
|
116
|
+
/** An action's capability: the static map, except that a pack action flagged `requiresTrust` needs the device trust list. */
|
|
117
|
+
function capabilityFor(action) {
|
|
118
|
+
if ((action.type === 'installPack' ||
|
|
119
|
+
action.type === 'loadPacks' ||
|
|
120
|
+
action.type === 'removePack') &&
|
|
121
|
+
action.requiresTrust === true) {
|
|
122
|
+
return 'pack-trust';
|
|
123
|
+
}
|
|
124
|
+
return ACTION_CAPABILITY[action.type];
|
|
125
|
+
}
|
|
126
|
+
function exportFormatFor(action) {
|
|
127
|
+
return action.type === 'exportAnsi' ? 'ansi' : 'html';
|
|
128
|
+
}
|
|
129
|
+
/** Whether `adapter` declares the capability `action` needs. Never consults the scenario or `expected.json` — only the adapter under test. */
|
|
130
|
+
export function hostDeclaresCapability(adapter, action) {
|
|
131
|
+
const capability = capabilityFor(action);
|
|
132
|
+
if (capability === undefined)
|
|
133
|
+
return true;
|
|
134
|
+
if (capability === 'pack-trust')
|
|
135
|
+
return usesPackTrust(adapter);
|
|
136
|
+
if (capability === 'isolate')
|
|
137
|
+
return adapter.isolate !== undefined;
|
|
138
|
+
if (capability === 'editor')
|
|
139
|
+
return adapter.editor !== undefined;
|
|
140
|
+
if (capability === 'packs') {
|
|
141
|
+
if (adapter.packs === undefined)
|
|
142
|
+
return false;
|
|
143
|
+
if (action.type === 'installPack') {
|
|
144
|
+
return adapter.packs.installRoot !== undefined;
|
|
145
|
+
}
|
|
146
|
+
if (action.type === 'removePack') {
|
|
147
|
+
return (adapter.packs.installRoot !== undefined &&
|
|
148
|
+
adapter.packs.removeFolder !== undefined);
|
|
149
|
+
}
|
|
150
|
+
return true;
|
|
151
|
+
}
|
|
152
|
+
// 'export-format'
|
|
153
|
+
return adapter.exports?.formats.has(exportFormatFor(action)) ?? false;
|
|
154
|
+
}
|
|
155
|
+
// --- the scripted prompt queue --------------------------------------------
|
|
156
|
+
/**
|
|
157
|
+
* Flattens every `run`/`installPack` action's `answers` into one queue, in
|
|
158
|
+
* document order — the same order the grant/consent prompts fire in, since
|
|
159
|
+
* `expected.json`'s `prompts` array is itself the ordered proof of that.
|
|
160
|
+
* `next()` THROWS once exhausted rather than returning a default: an
|
|
161
|
+
* unanswered prompt means the scenario and the implementation disagree
|
|
162
|
+
* about how many questions get asked, which must never be silently read
|
|
163
|
+
* as a deny.
|
|
164
|
+
*/
|
|
165
|
+
export function createAnswerQueue(scenario) {
|
|
166
|
+
const answers = [];
|
|
167
|
+
for (const action of scenario.actions) {
|
|
168
|
+
const source = action.type === 'run' ||
|
|
169
|
+
action.type === 'installPack' ||
|
|
170
|
+
action.type === 'enablePack'
|
|
171
|
+
? action.answers
|
|
172
|
+
: undefined;
|
|
173
|
+
for (const answer of source ?? [])
|
|
174
|
+
answers.push(answer === 'allow');
|
|
175
|
+
}
|
|
176
|
+
let index = 0;
|
|
177
|
+
return {
|
|
178
|
+
next() {
|
|
179
|
+
if (index >= answers.length) {
|
|
180
|
+
throw new Error(`scripted prompt queue exhausted after ${String(index)} answer(s): ` +
|
|
181
|
+
'the scenario and the implementation disagree about how many ' +
|
|
182
|
+
'questions get asked.');
|
|
183
|
+
}
|
|
184
|
+
return answers[index++];
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
// --- simulated execution ---------------------------------------------------
|
|
189
|
+
function simulatedRunResult(sim, options) {
|
|
190
|
+
const name = sim?.name ?? 'value';
|
|
191
|
+
if (sim?.requiresManualTier === true && options.trigger !== 'manual') {
|
|
192
|
+
return {
|
|
193
|
+
values: {},
|
|
194
|
+
failures: [
|
|
195
|
+
{
|
|
196
|
+
name,
|
|
197
|
+
message: `${name}: a write was attempted under the read-only "${options.trigger ?? 'manual'}" tier.`,
|
|
198
|
+
kind: 'tier-blocked',
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
if (sim?.requiresHost !== undefined &&
|
|
205
|
+
!options.netAllowlist.includes(sim.requiresHost)) {
|
|
206
|
+
return {
|
|
207
|
+
values: {},
|
|
208
|
+
failures: [
|
|
209
|
+
{
|
|
210
|
+
name,
|
|
211
|
+
message: `${name}: host "${sim.requiresHost}" was not granted.`,
|
|
212
|
+
kind: 'capability-denied',
|
|
213
|
+
},
|
|
214
|
+
],
|
|
215
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
values: { [name]: { status: 'fresh', value: sim?.value ?? 1 } },
|
|
220
|
+
failures: [],
|
|
221
|
+
cacheSnapshot: options.cacheSnapshot,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/** `unsupported()` in `./create-host.js` writes exactly this shape of line; scenarios describe the fully-capable host's behavior, so a decline line is not something `expected.json` should have to also state per host. */
|
|
225
|
+
const UNSUPPORTED_DIAGNOSTIC_LINE = /^declined \([a-z-]+ unsupported on /;
|
|
226
|
+
function decodeWritten(writtenFiles, path) {
|
|
227
|
+
if (path === undefined)
|
|
228
|
+
return undefined;
|
|
229
|
+
const bytes = writtenFiles.get(path);
|
|
230
|
+
return bytes === undefined ? undefined : new TextDecoder().decode(bytes);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Runs `scenario` against `adapter` end to end. `adapter` is the REAL,
|
|
234
|
+
* already-constructed host adapter under test (built by the caller from
|
|
235
|
+
* its own injected dependencies plus a scripted prompt source, e.g.
|
|
236
|
+
* `createAnswerQueue`); this function never builds one itself, so it works
|
|
237
|
+
* identically for any `HostAdapter`.
|
|
238
|
+
*/
|
|
239
|
+
export async function runHostScenario(adapter, scenario, noteText, scenarioDir, options = {}) {
|
|
240
|
+
const diagnostics = [];
|
|
241
|
+
const prompts = [];
|
|
242
|
+
const writtenFiles = new Map();
|
|
243
|
+
let promptQueueError;
|
|
244
|
+
const recording = {
|
|
245
|
+
...adapter,
|
|
246
|
+
now: () => scenario.clock,
|
|
247
|
+
diagnostics: (line) => {
|
|
248
|
+
diagnostics.push(line);
|
|
249
|
+
adapter.diagnostics(line);
|
|
250
|
+
},
|
|
251
|
+
prompt: async (request) => {
|
|
252
|
+
prompts.push(request.message);
|
|
253
|
+
return adapter.prompt(request);
|
|
254
|
+
},
|
|
255
|
+
writeFile: async (path, bytes) => {
|
|
256
|
+
writtenFiles.set(path, bytes);
|
|
257
|
+
await adapter.writeFile(path, bytes);
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
let currentSimulation;
|
|
261
|
+
const host = createMarkiiHost(recording, {
|
|
262
|
+
// The runner is Node-only test tooling, so it supplies the real Node
|
|
263
|
+
// implementations of the seams the host layer takes by injection.
|
|
264
|
+
htmlEngine: createHtmlExportEngine(),
|
|
265
|
+
archiveExtractFs: options.archiveExtractFs ?? createNodeArchiveExtractFs(),
|
|
266
|
+
spawnRun: (spawnOptions) => Promise.resolve(simulatedRunResult(currentSimulation, spawnOptions)),
|
|
267
|
+
...(options.renderAnsi ? { renderAnsi: options.renderAnsi } : {}),
|
|
268
|
+
});
|
|
269
|
+
const notePath = join(scenarioDir, 'note.mk.md');
|
|
270
|
+
const outcomes = [];
|
|
271
|
+
for (const action of scenario.actions) {
|
|
272
|
+
try {
|
|
273
|
+
outcomes.push(await runOneAction(host, recording, action, {
|
|
274
|
+
noteText,
|
|
275
|
+
notePath,
|
|
276
|
+
scenarioDir,
|
|
277
|
+
writtenFiles,
|
|
278
|
+
setSimulation: (sim) => {
|
|
279
|
+
currentSimulation = sim;
|
|
280
|
+
},
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
catch (err) {
|
|
284
|
+
promptQueueError = err instanceof Error ? err.message : String(err);
|
|
285
|
+
break;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
return {
|
|
289
|
+
outcomes,
|
|
290
|
+
diagnostics,
|
|
291
|
+
prompts,
|
|
292
|
+
...(promptQueueError !== undefined ? { promptQueueError } : {}),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
async function runOneAction(host, adapter, action, ctx) {
|
|
296
|
+
// A trust-scenario action on a host without the trust list never reaches
|
|
297
|
+
// the host: the declaration is what the corpus records.
|
|
298
|
+
if (capabilityFor(action) === 'pack-trust' && !usesPackTrust(adapter)) {
|
|
299
|
+
const detail = 'this host does not gate installed packs on a trust list.';
|
|
300
|
+
adapter.diagnostics(`declined (pack-trust unsupported on ${adapter.id}): ${detail}`);
|
|
301
|
+
return { kind: 'unsupported', capability: 'pack-trust', detail };
|
|
302
|
+
}
|
|
303
|
+
switch (action.type) {
|
|
304
|
+
case 'open':
|
|
305
|
+
return host.open({ path: ctx.notePath });
|
|
306
|
+
case 'run': {
|
|
307
|
+
if (action.scriptsDisabled === true) {
|
|
308
|
+
// The device-local scripts-off switch is not a `MarkiiHost`
|
|
309
|
+
// capability (see this file's top comment): it is decided at each
|
|
310
|
+
// app's own call site, before `host.run` is ever reached, using
|
|
311
|
+
// the shared wording in `./script-execution.js`. Reproduced here
|
|
312
|
+
// over the SAME functions and the adapter's own `labels` so the
|
|
313
|
+
// scenario proves wording parity without inventing a capability.
|
|
314
|
+
adapter.diagnostics(scriptsDisabledDiagnosticLine(action.trigger, adapter.labels));
|
|
315
|
+
return {
|
|
316
|
+
kind: 'declined',
|
|
317
|
+
reason: scriptsDisabledNotice(action.trigger, adapter.labels) ??
|
|
318
|
+
scriptsDisabledDiagnosticLine(action.trigger, adapter.labels),
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
ctx.setSimulation(action.simulate);
|
|
322
|
+
return host.run({
|
|
323
|
+
documentKey: ctx.notePath,
|
|
324
|
+
text: ctx.noteText,
|
|
325
|
+
trigger: action.trigger,
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
case 'exportHtml':
|
|
329
|
+
case 'exportAnsi': {
|
|
330
|
+
const outcome = await host.exportNote({
|
|
331
|
+
format: exportFormatFor(action),
|
|
332
|
+
notePath: ctx.notePath,
|
|
333
|
+
text: ctx.noteText,
|
|
334
|
+
});
|
|
335
|
+
return augmentExportOutcome(outcome, ctx.writtenFiles);
|
|
336
|
+
}
|
|
337
|
+
case 'installPack': {
|
|
338
|
+
const archiveBytes = zipArchiveDirectory(join(ctx.scenarioDir, action.archive));
|
|
339
|
+
return host.installPack({ archiveBytes, archivePath: action.archive });
|
|
340
|
+
}
|
|
341
|
+
case 'loadPacks':
|
|
342
|
+
return host.loadPacks();
|
|
343
|
+
case 'removePack':
|
|
344
|
+
return host.removePack(action.namespace);
|
|
345
|
+
case 'placePack': {
|
|
346
|
+
const installRoot = adapter.packs?.installRoot?.();
|
|
347
|
+
if (installRoot === undefined)
|
|
348
|
+
return { kind: 'failed' };
|
|
349
|
+
const files = readDirRecursive(join(ctx.scenarioDir, action.archive), join(ctx.scenarioDir, action.archive));
|
|
350
|
+
for (const [name, bytes] of Object.entries(files)) {
|
|
351
|
+
await adapter.writeFile(join(installRoot, action.namespace, ...name.split('/')), bytes);
|
|
352
|
+
}
|
|
353
|
+
return { kind: 'placed', namespace: action.namespace };
|
|
354
|
+
}
|
|
355
|
+
case 'listPacks':
|
|
356
|
+
return host.installedPacks();
|
|
357
|
+
case 'enablePack':
|
|
358
|
+
return host.enablePack(action.namespace);
|
|
359
|
+
case 'disablePack':
|
|
360
|
+
return host.disablePack(action.namespace);
|
|
361
|
+
case 'completeAt':
|
|
362
|
+
return host.completeAt({ line: action.line, column: action.column });
|
|
363
|
+
case 'hoverAt':
|
|
364
|
+
return host.hoverAt({ line: action.line, column: action.column });
|
|
365
|
+
case 'insertComponent':
|
|
366
|
+
return host.insertComponent({ name: action.name });
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function augmentExportOutcome(outcome, writtenFiles) {
|
|
370
|
+
if (outcome.kind !== 'exported')
|
|
371
|
+
return outcome;
|
|
372
|
+
return { ...outcome, content: decodeWritten(writtenFiles, outcome.path) };
|
|
373
|
+
}
|
|
374
|
+
// --- diffing against expected.json -----------------------------------------
|
|
375
|
+
/** `expected[i]` compared against `actual[i]` for each index up to `expected`'s own length; a length mismatch is reported once rather than index-by-index noise. */
|
|
376
|
+
export function containsInOrder(actual, expected) {
|
|
377
|
+
if (actual.length !== expected.length) {
|
|
378
|
+
return [
|
|
379
|
+
`count: expected ${String(expected.length)}, got ${String(actual.length)} ` +
|
|
380
|
+
`(actual: ${JSON.stringify(actual)})`,
|
|
381
|
+
];
|
|
382
|
+
}
|
|
383
|
+
const mismatches = [];
|
|
384
|
+
expected.forEach((exp, i) => {
|
|
385
|
+
if (!(actual[i] ?? '').includes(exp)) {
|
|
386
|
+
mismatches.push(`[${String(i)}]: expected to contain "${exp}", got "${actual[i]}"`);
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
return mismatches;
|
|
390
|
+
}
|
|
391
|
+
/** Extracts `{name: value}` for every non-error `StoredValue` in a `'ran'` outcome's `values`, for a plain-data comparison against `expected.json`. */
|
|
392
|
+
function landedValues(values) {
|
|
393
|
+
if (typeof values !== 'object' || values === null)
|
|
394
|
+
return {};
|
|
395
|
+
const out = {};
|
|
396
|
+
for (const [name, entry] of Object.entries(values)) {
|
|
397
|
+
if (typeof entry === 'object' &&
|
|
398
|
+
entry !== null &&
|
|
399
|
+
'status' in entry &&
|
|
400
|
+
entry.status !== 'error' &&
|
|
401
|
+
'value' in entry) {
|
|
402
|
+
out[name] = entry.value;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
return out;
|
|
406
|
+
}
|
|
407
|
+
function failureKinds(failures) {
|
|
408
|
+
if (!Array.isArray(failures))
|
|
409
|
+
return [];
|
|
410
|
+
return failures.map((f) => typeof f === 'object' && f !== null && 'kind' in f
|
|
411
|
+
? String(f.kind)
|
|
412
|
+
: 'unknown');
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* A coarse, per-`kind` comparison between one action's real outcome and its
|
|
416
|
+
* `expected.json` entry. Deliberately partial (matching the design's own
|
|
417
|
+
* "deliberately coarse" posture): a `'ran'` outcome compares landed values
|
|
418
|
+
* and failure KINDS, never timings or raw messages; an `'exported'` outcome
|
|
419
|
+
* compares `contains`/`excludes` substrings against the written file's
|
|
420
|
+
* content, never the whole document.
|
|
421
|
+
*/
|
|
422
|
+
export function outcomeMismatches(actual, expected) {
|
|
423
|
+
if (expected === undefined)
|
|
424
|
+
return [];
|
|
425
|
+
const actualRecord = typeof actual === 'object' && actual !== null
|
|
426
|
+
? actual
|
|
427
|
+
: {};
|
|
428
|
+
if (actualRecord['kind'] !== expected['kind']) {
|
|
429
|
+
return [
|
|
430
|
+
`kind: expected "${String(expected['kind'])}", got "${String(actualRecord['kind'])}"`,
|
|
431
|
+
];
|
|
432
|
+
}
|
|
433
|
+
const mismatches = [];
|
|
434
|
+
switch (expected['kind']) {
|
|
435
|
+
case 'ran': {
|
|
436
|
+
const wantValues = expected['values'] ?? {};
|
|
437
|
+
const gotValues = landedValues(actualRecord['values']);
|
|
438
|
+
if (JSON.stringify(gotValues) !== JSON.stringify(wantValues)) {
|
|
439
|
+
mismatches.push(`values: expected ${JSON.stringify(wantValues)}, got ${JSON.stringify(gotValues)}`);
|
|
440
|
+
}
|
|
441
|
+
const wantKinds = expected['failureKinds'] ?? [];
|
|
442
|
+
const gotKinds = failureKinds(actualRecord['failures']);
|
|
443
|
+
if (JSON.stringify(gotKinds) !== JSON.stringify(wantKinds)) {
|
|
444
|
+
mismatches.push(`failure kinds: expected ${JSON.stringify(wantKinds)}, got ${JSON.stringify(gotKinds)}`);
|
|
445
|
+
}
|
|
446
|
+
if (expected['traceOk'] !== undefined) {
|
|
447
|
+
const trace = actualRecord['trace'];
|
|
448
|
+
if (trace?.ok !== expected['traceOk']) {
|
|
449
|
+
mismatches.push(`trace.ok: expected ${String(expected['traceOk'])}, got ${String(trace?.ok)}`);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
break;
|
|
453
|
+
}
|
|
454
|
+
case 'exported': {
|
|
455
|
+
const content = String(actualRecord['content'] ?? '');
|
|
456
|
+
for (const needle of expected['contains'] ?? []) {
|
|
457
|
+
if (!content.includes(needle)) {
|
|
458
|
+
mismatches.push(`export should contain "${needle}"`);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
for (const needle of expected['excludes'] ?? []) {
|
|
462
|
+
if (content.includes(needle)) {
|
|
463
|
+
mismatches.push(`export should not contain "${needle}"`);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
case 'installed': {
|
|
469
|
+
if (expected['namespace'] !== undefined &&
|
|
470
|
+
actualRecord['namespace'] !== expected['namespace']) {
|
|
471
|
+
mismatches.push(`namespace: expected "${String(expected['namespace'])}", got "${String(actualRecord['namespace'])}"`);
|
|
472
|
+
}
|
|
473
|
+
if (expected['replaced'] !== undefined &&
|
|
474
|
+
actualRecord['replaced'] !== expected['replaced']) {
|
|
475
|
+
mismatches.push(`replaced: expected ${String(expected['replaced'])}, got ${String(actualRecord['replaced'])}`);
|
|
476
|
+
}
|
|
477
|
+
break;
|
|
478
|
+
}
|
|
479
|
+
case 'declined': {
|
|
480
|
+
if (expected['step'] !== undefined &&
|
|
481
|
+
actualRecord['step'] !== expected['step']) {
|
|
482
|
+
mismatches.push(`step: expected "${String(expected['step'])}", got "${String(actualRecord['step'])}"`);
|
|
483
|
+
}
|
|
484
|
+
if (expected['reasonContains'] !== undefined &&
|
|
485
|
+
!String(actualRecord['reason'] ?? '').includes(String(expected['reasonContains']))) {
|
|
486
|
+
mismatches.push(`reason: expected to contain "${String(expected['reasonContains'])}", got "${String(actualRecord['reason'])}"`);
|
|
487
|
+
}
|
|
488
|
+
break;
|
|
489
|
+
}
|
|
490
|
+
case 'listed': {
|
|
491
|
+
const names = (list, pick) => (Array.isArray(list) ? list.map(pick) : []).map(String).sort();
|
|
492
|
+
const gotEnabled = names(actualRecord['enabled'], (entry) => entry.namespace);
|
|
493
|
+
const gotNotEnabled = names(actualRecord['notEnabled'], (entry) => entry);
|
|
494
|
+
for (const [key, got] of [
|
|
495
|
+
['enabled', gotEnabled],
|
|
496
|
+
['notEnabled', gotNotEnabled],
|
|
497
|
+
]) {
|
|
498
|
+
const want = [...(expected[key] ?? [])].sort();
|
|
499
|
+
if (JSON.stringify(got) !== JSON.stringify(want)) {
|
|
500
|
+
mismatches.push(`${key}: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
break;
|
|
504
|
+
}
|
|
505
|
+
case 'enabled':
|
|
506
|
+
case 'disabled':
|
|
507
|
+
case 'not-enabled':
|
|
508
|
+
case 'placed':
|
|
509
|
+
case 'removed': {
|
|
510
|
+
if (expected['namespace'] !== undefined &&
|
|
511
|
+
actualRecord['namespace'] !== expected['namespace']) {
|
|
512
|
+
mismatches.push(`namespace: expected "${String(expected['namespace'])}", got "${String(actualRecord['namespace'])}"`);
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
case 'loaded': {
|
|
517
|
+
const want = [...(expected['namespaces'] ?? [])].sort();
|
|
518
|
+
const got = [...(actualRecord['namespaces'] ?? [])].sort();
|
|
519
|
+
if (JSON.stringify(got) !== JSON.stringify(want)) {
|
|
520
|
+
mismatches.push(`namespaces: expected ${JSON.stringify(want)}, got ${JSON.stringify(got)}`);
|
|
521
|
+
}
|
|
522
|
+
// The component counts are engine-neutral on purpose: a pack with one
|
|
523
|
+
// view per engine has exactly one component unavailable on every host.
|
|
524
|
+
for (const [expectedKey, actualKey] of [
|
|
525
|
+
['componentCount', 'components'],
|
|
526
|
+
['unavailableCount', 'unavailable'],
|
|
527
|
+
]) {
|
|
528
|
+
if (expected[expectedKey] === undefined)
|
|
529
|
+
continue;
|
|
530
|
+
const list = actualRecord[actualKey];
|
|
531
|
+
const count = Array.isArray(list) ? list.length : 0;
|
|
532
|
+
if (count !== expected[expectedKey]) {
|
|
533
|
+
mismatches.push(`${expectedKey}: expected ${String(expected[expectedKey])}, got ${String(count)}`);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
break;
|
|
537
|
+
}
|
|
538
|
+
case 'completions': {
|
|
539
|
+
const got = actualRecord['labels'] ?? [];
|
|
540
|
+
for (const label of expected['labels'] ?? []) {
|
|
541
|
+
if (!got.includes(label)) {
|
|
542
|
+
mismatches.push(`completions should include "${label}"`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
for (const key of ['replaceStart', 'replaceEnd', 'tokenStart']) {
|
|
546
|
+
if (expected[key] !== undefined &&
|
|
547
|
+
actualRecord[key] !== expected[key]) {
|
|
548
|
+
mismatches.push(`${key}: expected ${String(expected[key])}, got ${String(actualRecord[key])}`);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
if (expected['fenceEditCount'] !== undefined) {
|
|
552
|
+
const got = Array.isArray(actualRecord['fenceEdits'])
|
|
553
|
+
? actualRecord['fenceEdits'].length
|
|
554
|
+
: 0;
|
|
555
|
+
if (got !== expected['fenceEditCount']) {
|
|
556
|
+
mismatches.push(`fenceEditCount: expected ${String(expected['fenceEditCount'])}, got ${String(got)}`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case 'hover': {
|
|
562
|
+
const contains = expected['contains'];
|
|
563
|
+
if (contains !== undefined &&
|
|
564
|
+
!String(actualRecord['text'] ?? '').includes(contains)) {
|
|
565
|
+
mismatches.push(`hover text should contain "${contains}"`);
|
|
566
|
+
}
|
|
567
|
+
for (const key of ['start', 'end']) {
|
|
568
|
+
if (expected[key] !== undefined &&
|
|
569
|
+
actualRecord[key] !== expected[key]) {
|
|
570
|
+
mismatches.push(`${key}: expected ${String(expected[key])}, got ${String(actualRecord[key])}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
break;
|
|
574
|
+
}
|
|
575
|
+
default:
|
|
576
|
+
break; // kind equality is the whole check for every other outcome.
|
|
577
|
+
}
|
|
578
|
+
return mismatches;
|
|
579
|
+
}
|
|
580
|
+
/**
|
|
581
|
+
* The full assertion an app's conformance test makes for one scenario: for
|
|
582
|
+
* every action whose capability `adapter` declares, its outcome matches
|
|
583
|
+
* `expected`'s; for every other action, the outcome is exactly the shared
|
|
584
|
+
* `unsupported` record. Returns a list of human-readable problems — empty
|
|
585
|
+
* means the scenario passed.
|
|
586
|
+
*/
|
|
587
|
+
export function assertHostScenario(adapter, scenario, expected, result) {
|
|
588
|
+
const problems = [];
|
|
589
|
+
if (result.promptQueueError !== undefined) {
|
|
590
|
+
problems.push(`prompt queue: ${result.promptQueueError}`);
|
|
591
|
+
}
|
|
592
|
+
scenario.actions.forEach((action, i) => {
|
|
593
|
+
const actual = result.outcomes[i];
|
|
594
|
+
const declared = hostDeclaresCapability(adapter, action);
|
|
595
|
+
if (!declared) {
|
|
596
|
+
const capability = capabilityFor(action);
|
|
597
|
+
const actualRecord = typeof actual === 'object' && actual !== null
|
|
598
|
+
? actual
|
|
599
|
+
: {};
|
|
600
|
+
if (actualRecord['kind'] !== 'unsupported' ||
|
|
601
|
+
actualRecord['capability'] !== capability) {
|
|
602
|
+
problems.push(`action ${String(i)} (${action.type}): expected {kind:"unsupported", capability:"${String(capability)}"}, got ${JSON.stringify(actual)}`);
|
|
603
|
+
}
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
for (const problem of outcomeMismatches(actual, expected.outcomes[i])) {
|
|
607
|
+
problems.push(`action ${String(i)} (${action.type}): ${problem}`);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
// `expected.json`'s `diagnostics`/`prompts` describe what a FULLY CAPABLE
|
|
611
|
+
// host does. When every action in this scenario needs a capability the
|
|
612
|
+
// adapter lacks (a whole scenario built around one capability domain,
|
|
613
|
+
// e.g. every action in `06-install-pack-then-render` needs `packs`),
|
|
614
|
+
// nothing meaningful ran and there is nothing to compare — the
|
|
615
|
+
// per-action `unsupported` check above already covers this host
|
|
616
|
+
// honestly. Comparing an unrelated host's empty prompt/diagnostics list
|
|
617
|
+
// against a capable host's expectation here would be double-counting
|
|
618
|
+
// the same capability gap as a second, spurious failure.
|
|
619
|
+
const capabilityGatedActions = scenario.actions.filter((action) => capabilityFor(action) !== undefined);
|
|
620
|
+
const anyActionDeclared = capabilityGatedActions.length === 0 ||
|
|
621
|
+
capabilityGatedActions.some((action) => hostDeclaresCapability(adapter, action));
|
|
622
|
+
if (anyActionDeclared) {
|
|
623
|
+
const filteredDiagnostics = result.diagnostics.filter((line) => !UNSUPPORTED_DIAGNOSTIC_LINE.test(line));
|
|
624
|
+
for (const problem of containsInOrder(filteredDiagnostics, expected.diagnostics)) {
|
|
625
|
+
problems.push(`diagnostics ${problem}`);
|
|
626
|
+
}
|
|
627
|
+
for (const problem of containsInOrder(result.prompts, expected.prompts)) {
|
|
628
|
+
problems.push(`prompts ${problem}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
return problems;
|
|
632
|
+
}
|