@markii/host 0.17.0-next.4 → 0.17.0-next.6
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 +28 -1
- package/dist/browser-worker.classic.js +43 -43
- package/dist/browser-worker.js +43 -43
- package/dist/conformance/host/17-export-linked-set/expected.json +17 -0
- package/dist/conformance/host/17-export-linked-set/files/second.mk.md +3 -0
- package/dist/conformance/host/17-export-linked-set/note.mk.md +4 -0
- package/dist/conformance/host/17-export-linked-set/scenario.json +6 -0
- package/dist/conformance/host/18-last-run-and-grants/expected.json +42 -0
- package/dist/conformance/host/18-last-run-and-grants/note.mk.md +5 -0
- package/dist/conformance/host/18-last-run-and-grants/scenario.json +27 -0
- package/dist/conformance/host/README.md +24 -14
- package/dist/conformance.d.ts +1 -1
- package/dist/conformance.js +1 -1
- package/dist/diagnostics/render-diagnostics.d.ts +1 -1
- package/dist/host/adapter.d.ts +28 -6
- package/dist/host/cascade-export-behavior.d.ts +26 -8
- package/dist/host/cascade-export-behavior.js +64 -1
- package/dist/host/conformance-runner.d.ts +11 -1
- package/dist/host/conformance-runner.js +80 -4
- package/dist/host/create-host.d.ts +250 -44
- package/dist/host/create-host.js +222 -96
- package/dist/host/export-behavior.d.ts +2 -0
- package/dist/host/export-behavior.js +10 -4
- package/dist/host/pack-archive.d.ts +7 -6
- package/dist/host/pack-archive.js +11 -7
- package/dist/host/pack-install.js +9 -7
- package/dist/host/pack-remove.d.ts +7 -18
- package/dist/host/pack-remove.js +2 -0
- package/dist/host/pack-trust-behavior.d.ts +13 -36
- package/dist/host/pack-trust-behavior.js +23 -2
- package/dist/host/run-behavior.d.ts +25 -34
- package/dist/host/run-behavior.js +31 -5
- package/dist/host/terminal-packs.d.ts +1 -1
- package/dist/index.d.ts +8 -10
- package/dist/index.js +1 -1
- package/dist/internal.d.ts +10 -5
- package/dist/internal.js +4 -3
- package/dist/node-worker.js +3 -1
- package/dist/node.d.ts +1 -2
- package/dist/node.js +1 -1
- package/dist/run/bundle-run.d.ts +1 -1
- package/dist/run/bundle-run.js +1 -1
- package/dist/run/node-isolate.d.ts +6 -2
- package/dist/run/node-isolate.js +2 -0
- package/dist/run/run-host.d.ts +2 -2
- package/dist/run/worker-entry-browser.d.ts +6 -1
- package/dist/run/worker-entry-browser.js +62 -5
- package/package.json +11 -11
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"outcomes": [
|
|
3
|
+
{
|
|
4
|
+
"kind": "exported",
|
|
5
|
+
"noteCount": 2,
|
|
6
|
+
"entries": ["index.html", "note.html", "second.html"]
|
|
7
|
+
}
|
|
8
|
+
],
|
|
9
|
+
"diagnostics": [
|
|
10
|
+
"Cascade export wrote",
|
|
11
|
+
"Exported",
|
|
12
|
+
"Rendered with the static engine because no pack components are loaded",
|
|
13
|
+
"Exported",
|
|
14
|
+
"Rendered with the static engine because no pack components are loaded"
|
|
15
|
+
],
|
|
16
|
+
"prompts": []
|
|
17
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"outcomes": [
|
|
3
|
+
{ "kind": "lastRun", "values": {} },
|
|
4
|
+
{
|
|
5
|
+
"kind": "ran",
|
|
6
|
+
"values": { "total": 3 },
|
|
7
|
+
"failureKinds": [],
|
|
8
|
+
"traceOk": true
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"kind": "lastRun",
|
|
12
|
+
"values": { "total": 3 },
|
|
13
|
+
"valueStatuses": { "total": "stale" },
|
|
14
|
+
"traceOk": true
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"kind": "ran",
|
|
18
|
+
"values": { "total": 3 },
|
|
19
|
+
"failureKinds": [],
|
|
20
|
+
"traceOk": true
|
|
21
|
+
},
|
|
22
|
+
{ "kind": "forgotten" },
|
|
23
|
+
{
|
|
24
|
+
"kind": "ran",
|
|
25
|
+
"values": { "total": 3 },
|
|
26
|
+
"failureKinds": [],
|
|
27
|
+
"traceOk": true
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
"diagnostics": [
|
|
31
|
+
"manual run: net declaration mismatch",
|
|
32
|
+
"uses net access to api.example.com",
|
|
33
|
+
"manual run: net declaration mismatch",
|
|
34
|
+
"uses net access to api.example.com",
|
|
35
|
+
"manual run: net declaration mismatch",
|
|
36
|
+
"uses net access to api.example.com"
|
|
37
|
+
],
|
|
38
|
+
"prompts": [
|
|
39
|
+
"This note's scripts can send data to api.example.com. Allow?",
|
|
40
|
+
"This note's scripts can send data to api.example.com. Allow?"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "last-run-and-grants",
|
|
3
|
+
"invariant": "a run's values and trace can be read back with no storage key, and clearing the stored grants makes the next manual run prompt again",
|
|
4
|
+
"clock": 1700000000000,
|
|
5
|
+
"actions": [
|
|
6
|
+
{ "type": "lastRun" },
|
|
7
|
+
{
|
|
8
|
+
"type": "run",
|
|
9
|
+
"trigger": "manual",
|
|
10
|
+
"answers": ["allow"],
|
|
11
|
+
"simulate": { "name": "total", "value": 3, "requiresHost": "api.example.com" }
|
|
12
|
+
},
|
|
13
|
+
{ "type": "lastRun" },
|
|
14
|
+
{
|
|
15
|
+
"type": "run",
|
|
16
|
+
"trigger": "manual",
|
|
17
|
+
"simulate": { "name": "total", "value": 3, "requiresHost": "api.example.com" }
|
|
18
|
+
},
|
|
19
|
+
{ "type": "forgetGrants" },
|
|
20
|
+
{
|
|
21
|
+
"type": "run",
|
|
22
|
+
"trigger": "manual",
|
|
23
|
+
"answers": ["allow"],
|
|
24
|
+
"simulate": { "name": "total", "value": 3, "requiresHost": "api.example.com" }
|
|
25
|
+
}
|
|
26
|
+
]
|
|
27
|
+
}
|
|
@@ -47,8 +47,11 @@ needs, is the runner's own `ACTION_CAPABILITY` map:
|
|
|
47
47
|
|---|---|
|
|
48
48
|
| `open` | (always supported) |
|
|
49
49
|
| `run` | `isolate` |
|
|
50
|
+
| `lastRun` | (always supported) |
|
|
51
|
+
| `forgetGrants` | (always supported) |
|
|
50
52
|
| `exportHtml` | `export-format` (`html`) |
|
|
51
53
|
| `exportAnsi` | `export-format` (`ansi`) |
|
|
54
|
+
| `exportLinkedSet` | `linked-set-export` (a `resolveLinkedSetTarget`) |
|
|
52
55
|
| `installPack` | `packs` (and an `installRoot`) |
|
|
53
56
|
| `loadPacks` | `packs` |
|
|
54
57
|
| `removePack` | `packs` (and an `installRoot` plus `removeFolder`) |
|
|
@@ -97,12 +100,12 @@ would have produced:
|
|
|
97
100
|
and persists exactly what it always does. Only the isolate that would
|
|
98
101
|
execute the script is faked.
|
|
99
102
|
- `requiresManualTier`: the run reports `tier-blocked` under any trigger
|
|
100
|
-
but `'manual'`, regardless of grants
|
|
103
|
+
but `'manual'`, regardless of grants, the same enforcement the real
|
|
101
104
|
sandbox applies to an effectful call under a read-only trigger.
|
|
102
105
|
|
|
103
106
|
Faking the isolate keeps this corpus fast and deterministic. It proves the
|
|
104
|
-
HOST-LEVEL plumbing
|
|
105
|
-
trigger forwarding
|
|
107
|
+
HOST-LEVEL plumbing, grant prompts, wording, capability declarations,
|
|
108
|
+
trigger forwarding, is identical across hosts. Real sandbox enforcement
|
|
106
109
|
(the tier gate, the net allowlist, the marshal boundary) is
|
|
107
110
|
`conformance/executor/`'s job and is not duplicated here.
|
|
108
111
|
|
|
@@ -136,15 +139,21 @@ coarse, matching each `kind`:
|
|
|
136
139
|
(`{name: value}`); `failureKinds` compares the closed `FailureKind`
|
|
137
140
|
taxonomy only, never a raw message or a timing; an optional `traceOk`
|
|
138
141
|
compares the recorded run trace's `ok` flag.
|
|
142
|
+
- `lastRun`: `values` compares the stored values shown on open, by name,
|
|
143
|
+
like `ran`. `valueStatuses` compares each value's status by name, and
|
|
144
|
+
`traceOk` compares the recorded trace's `ok` flag.
|
|
139
145
|
- `exported`: `contains`/`excludes` are substrings checked against the
|
|
140
|
-
written file's content, never the whole document
|
|
146
|
+
written file's content, never the whole document, the render corpora
|
|
141
147
|
already pin bytes. `valueCount` is how many last-run values were baked
|
|
142
148
|
in, and `renderEngine` (`static` or `react`) is which engine drew the body.
|
|
149
|
+
A linked-set export is an `exported` outcome too: `noteCount` is how many
|
|
150
|
+
notes the archive holds, and `entries` is its sorted file names.
|
|
143
151
|
- `installed`: `namespace` and `replaced`, and `installedDirName`, the name
|
|
144
152
|
of the folder the pack was unzipped into.
|
|
145
153
|
- `declined`: `step` (`'consent'`/`'replace'`, or absent for the
|
|
146
154
|
scripts-disabled synthetic outcome) and `reasonContains`, a substring of
|
|
147
|
-
the user-facing reason.
|
|
155
|
+
the user-facing reason. A user's "no" to a prompt and a cancelled picker
|
|
156
|
+
are both `declined`.
|
|
148
157
|
- `listed`: `enabled` and `notEnabled`, namespace lists, order-independent.
|
|
149
158
|
- `enabled`, `disabled`, `placed`, `removed`: `namespace`.
|
|
150
159
|
- `loaded`: `namespaces`, order-independent.
|
|
@@ -156,12 +165,12 @@ coarse, matching each `kind`:
|
|
|
156
165
|
accepted container item.
|
|
157
166
|
- `hover`: `contains`, a substring of the hover text. Optional `start` and
|
|
158
167
|
`end` compare the hovered name's columns.
|
|
159
|
-
- every other `kind` (`opened`, `none`, `not-found`, `failed`, `
|
|
160
|
-
`reserved`, `
|
|
168
|
+
- every other `kind` (`opened`, `none`, `not-found`, `failed`, `invalid`,
|
|
169
|
+
`reserved`, `forgotten`, `inserted`): `kind` equality is the whole check.
|
|
161
170
|
|
|
162
171
|
An action whose capability the adapter under test does not declare is
|
|
163
172
|
never diffed against `expected.json` at all (see "Capability handling"
|
|
164
|
-
below)
|
|
173
|
+
below), `expected.json` always describes the FULLY CAPABLE host's
|
|
165
174
|
correct behavior, even for a scenario where one real host today has
|
|
166
175
|
nothing to compare it to.
|
|
167
176
|
|
|
@@ -173,11 +182,11 @@ difference) without the corpus having to spell out three wordings for
|
|
|
173
182
|
one behavior. Diagnostics lines written by a capability decline itself
|
|
174
183
|
(`declined (<capability> unsupported on <host>): …`) are filtered out of
|
|
175
184
|
the actual list before this comparison, since that line exists only
|
|
176
|
-
because the capability was ABSENT
|
|
185
|
+
because the capability was ABSENT, it is not part of what
|
|
177
186
|
`expected.json` describes.
|
|
178
187
|
|
|
179
188
|
`prompts` is load-bearing: it proves all three hosts ask the same
|
|
180
|
-
questions, worded the same way, in the same order
|
|
189
|
+
questions, worded the same way, in the same order, the single most
|
|
181
190
|
security-relevant parity property in this batch.
|
|
182
191
|
|
|
183
192
|
## Capability handling (the crux)
|
|
@@ -197,7 +206,7 @@ run the whole corpus honestly: its result records say `unsupported`
|
|
|
197
206
|
where it genuinely offers nothing, which is the truth and is visible,
|
|
198
207
|
rather than a skipped test or a faked empty answer.
|
|
199
208
|
|
|
200
|
-
## The
|
|
209
|
+
## The scenarios
|
|
201
210
|
|
|
202
211
|
| id | what it proves |
|
|
203
212
|
|---|---|
|
|
@@ -217,7 +226,8 @@ rather than a skipped test or a faked empty answer.
|
|
|
217
226
|
| `14-remove-pack` | removing an installed pack deletes it; removing it again reports it is not installed |
|
|
218
227
|
| `15-load-packs-engine-views` | loading packs reports every installed component and, as `unavailable`, the ones this host's engine has no view for |
|
|
219
228
|
| `16-pack-trust` | a folder that arrives without consent is listed as present and never loaded; enable, disable, re-enable and remove move the device trust list; a host without the list reports the whole scenario `unsupported` |
|
|
229
|
+
| `17-export-linked-set` | a note and the notes it links to export as one archive with an index page; a host with no linked-set target reports `unsupported` |
|
|
230
|
+
| `18-last-run-and-grants` | a run's values and trace read back through `lastRun` with no storage key, and clearing the grants makes the next manual run prompt again |
|
|
220
231
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
`unsupported` on every host today (a reported gap, not a corpus defect).
|
|
232
|
+
The capabilities an adapter declares decide which scenarios report
|
|
233
|
+
`unsupported` on that host. That is a reported gap and not a corpus defect.
|
package/dist/conformance.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { assertHostScenario, createAnswerQueue, hostConformanceDir, listHostScenarioNames, loadHostScenario, runHostScenario, } from './host/conformance-runner.js';
|
|
2
2
|
export type { HostScenario, HostScenarioAction, HostScenarioCapability, HostScenarioExpected, HostScenarioRunResult, LoadedHostScenario, RunSimulation, } from './host/conformance-runner.js';
|
package/dist/conformance.js
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
// the corpus from disk and drives a real adapter). A host's adapter test
|
|
3
3
|
// imports this to run `conformance/host/` against its own `HostAdapter`.
|
|
4
4
|
// The corpus ships inside the package at `dist/conformance/host/`.
|
|
5
|
-
export {
|
|
5
|
+
export { assertHostScenario, createAnswerQueue, hostConformanceDir, listHostScenarioNames, loadHostScenario, runHostScenario, } from './host/conformance-runner.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The one place a render-time diagnostic event becomes a line on a host's
|
|
3
|
-
* diagnostics surface (docs/
|
|
3
|
+
* diagnostics surface (docs/hosts.md's host checklist, item 9).
|
|
4
4
|
*
|
|
5
5
|
* Both renderers report a quiet in-page marker through their `onDiagnostic`
|
|
6
6
|
* render option (`@markii/stdlib`'s `DiagnosticEvent`). The marker alone
|
package/dist/host/adapter.d.ts
CHANGED
|
@@ -54,9 +54,9 @@ export type HostIsolate = {
|
|
|
54
54
|
*/
|
|
55
55
|
workerPath(): string;
|
|
56
56
|
/**
|
|
57
|
-
* Creates the worker-thread isolate
|
|
58
|
-
* `@markii/host/node
|
|
59
|
-
* nothing from `node:*`.
|
|
57
|
+
* Creates the worker-thread isolate. `createNodeHostIsolate()` from
|
|
58
|
+
* `@markii/host/node` supplies it. Injected so the shared run path
|
|
59
|
+
* imports nothing from `node:*`.
|
|
60
60
|
*/
|
|
61
61
|
readonly spawnIsolate: IsolateSpawner;
|
|
62
62
|
} | {
|
|
@@ -155,12 +155,34 @@ export interface HostPackSource {
|
|
|
155
155
|
}
|
|
156
156
|
/** Every export format any host offers today. */
|
|
157
157
|
export type HostExportFormat = 'html' | 'pdf' | 'ansi' | 'md-plain';
|
|
158
|
+
/**
|
|
159
|
+
* What a host declares to offer exports. A request can carry its own
|
|
160
|
+
* `targetPath`, so a host with no picker (the command line tool, or a call
|
|
161
|
+
* site that already knows where the file goes) leaves the resolvers out.
|
|
162
|
+
*
|
|
163
|
+
* A single-note export needs `exports` declared with the format in
|
|
164
|
+
* `formats`, and a destination: either a `resolveTarget` or the request's
|
|
165
|
+
* `targetPath`. Without a destination it answers `unsupported`.
|
|
166
|
+
*
|
|
167
|
+
* The linked-set export is offered when `exports` is declared and the host
|
|
168
|
+
* has a `resolveLinkedSetTarget` or the request carries a `targetPath`.
|
|
169
|
+
* Without either it answers `unsupported`. A host that declares `exports`
|
|
170
|
+
* with neither resolver, such as the command line tool, therefore offers
|
|
171
|
+
* the linked-set export only to a caller that names the path.
|
|
172
|
+
*/
|
|
158
173
|
export interface HostExportCapabilities {
|
|
159
174
|
readonly formats: ReadonlySet<HostExportFormat>;
|
|
160
|
-
/** GUI hosts show a picker; the CLI returns the path it was given. */
|
|
161
|
-
resolveTarget(suggestedFileName: string, format: HostExportFormat): Promise<string | undefined>;
|
|
175
|
+
/** GUI hosts show a picker; the CLI returns the path it was given. Resolves `undefined` when the user cancels. Absent for a host whose callers always pass `targetPath`. */
|
|
176
|
+
resolveTarget?(suggestedFileName: string, format: HostExportFormat): Promise<string | undefined>;
|
|
162
177
|
/** Only present on a host declaring `'pdf'`. The one Electron-touching seam. */
|
|
163
178
|
htmlToPdf?(html: string): Promise<Uint8Array>;
|
|
179
|
+
/**
|
|
180
|
+
* Names the zip archive of a linked-set export, or resolves `undefined`
|
|
181
|
+
* when the user cancels. A host can offer single-note export without it
|
|
182
|
+
* (the command line tool does), so it is a member of its own rather than
|
|
183
|
+
* a `HostExportFormat`.
|
|
184
|
+
*/
|
|
185
|
+
resolveLinkedSetTarget?(suggestedFileName: string): Promise<string | undefined>;
|
|
164
186
|
}
|
|
165
187
|
/**
|
|
166
188
|
* The wording nouns every shared behavior's message templates interpolate
|
|
@@ -210,7 +232,7 @@ export interface HostAdapter {
|
|
|
210
232
|
* through with no adapter wrapper at all.
|
|
211
233
|
*/
|
|
212
234
|
readonly memento: GrantMemento;
|
|
213
|
-
/** One diagnostics line on this host's designated surface (docs/
|
|
235
|
+
/** One diagnostics line on this host's designated surface (docs/hosts.md). */
|
|
214
236
|
diagnostics(line: string): void;
|
|
215
237
|
now(): number;
|
|
216
238
|
readonly isolate?: HostIsolate;
|
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
* produces. Written once so the hosts cannot drift on what a cascade is
|
|
6
6
|
* or what it says.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* `createMarkiiHost`'s `exportLinkedSet` drives it: the host's picker
|
|
9
|
+
* names the archive's path (`adapter.exports.resolveLinkedSetTarget`), the
|
|
10
|
+
* notes are read through `adapter.readFile`, the archive is written through
|
|
11
|
+
* `adapter.writeFile`, and the diagnostics lines go to `adapter.diagnostics`.
|
|
12
|
+
* The per-host nouns (which surface holds the details) come from
|
|
13
|
+
* `HostLabels`, never from a literal here.
|
|
13
14
|
*
|
|
14
15
|
* BOUNDS ARE FIXED. The walk always uses `DEFAULT_CASCADE_MAX_DEPTH` and
|
|
15
16
|
* `DEFAULT_CASCADE_MAX_NOTES`; nothing here lets a caller override them.
|
|
@@ -19,7 +20,8 @@ import type { OnDiagnostic, UnavailableComponent } from '@markii/stdlib';
|
|
|
19
20
|
import type { CascadeLinkResolver, CascadeNoteReader, CascadeTruncation } from '../export/cascade.js';
|
|
20
21
|
import type { HtmlExportEngine, ExportBodyRenderer, ExportPackStylesheet, ExportRenderInfo, StaticExportReason } from '../export/note-export.js';
|
|
21
22
|
import type { EmbeddedImageReport, ExportImageReader } from '../export/image-embed.js';
|
|
22
|
-
import type { HostLabels } from './adapter.js';
|
|
23
|
+
import type { HostAdapter, HostLabels } from './adapter.js';
|
|
24
|
+
import type { ExportLinkedSetOutcome } from './create-host.js';
|
|
23
25
|
/** One note the cascade exported, for the diagnostics surface. */
|
|
24
26
|
export interface CascadeExportedNote {
|
|
25
27
|
/** The note's own path, in the same form the reader and resolver use. */
|
|
@@ -37,7 +39,7 @@ export interface UnreadableCascadeNote {
|
|
|
37
39
|
}
|
|
38
40
|
/** What one cascade export attempt did, for both the message and the diagnostics lines. */
|
|
39
41
|
export type CascadeExportOutcome = {
|
|
40
|
-
readonly kind: '
|
|
42
|
+
readonly kind: 'exported';
|
|
41
43
|
/** The written archive's display path. */
|
|
42
44
|
readonly path: string;
|
|
43
45
|
readonly bytes: number;
|
|
@@ -109,7 +111,7 @@ export declare function buildCascadeArchive(request: CascadeArchiveRequest): Pro
|
|
|
109
111
|
* failure says what failed and points at the same surface, never at a
|
|
110
112
|
* stack trace.
|
|
111
113
|
*/
|
|
112
|
-
export declare function cascadeResultMessage(outcome:
|
|
114
|
+
export declare function cascadeResultMessage(outcome: ExportLinkedSetOutcome, labels: HostLabels, where?: string): string;
|
|
113
115
|
/**
|
|
114
116
|
* The lines for the host's diagnostics surface for one cascade export:
|
|
115
117
|
* every note reached named with the file it became, every note skipped
|
|
@@ -118,3 +120,19 @@ export declare function cascadeResultMessage(outcome: CascadeExportOutcome, labe
|
|
|
118
120
|
* deliberately omits.
|
|
119
121
|
*/
|
|
120
122
|
export declare function cascadeDiagnosticLines(outcome: CascadeExportOutcome): string[];
|
|
123
|
+
/** What `exportLinkedSetViaAdapter` needs beyond the adapter: the static engine and the request's own seams. */
|
|
124
|
+
export interface LinkedSetExportInputs extends Omit<CascadeArchiveRequest, 'readNote'> {
|
|
125
|
+
/** The suggested archive file name handed to the host's picker. */
|
|
126
|
+
readonly suggestedFileName: string;
|
|
127
|
+
/** Where to write the archive. When present the adapter's `resolveLinkedSetTarget` is not asked. */
|
|
128
|
+
readonly targetPath?: string;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Exports a note and the notes it links to as one archive: resolves the
|
|
132
|
+
* target through the adapter's picker, builds the archive reading every
|
|
133
|
+
* note through `adapter.readFile`, writes it through `adapter.writeFile`,
|
|
134
|
+
* and sends the diagnostics lines to `adapter.diagnostics`. Resolves
|
|
135
|
+
* `undefined` when the host has no linked-set target or its picker was
|
|
136
|
+
* cancelled. A `targetPath` skips the picker. Never throws.
|
|
137
|
+
*/
|
|
138
|
+
export declare function exportLinkedSetViaAdapter(adapter: HostAdapter, inputs: LinkedSetExportInputs): Promise<CascadeExportOutcome | undefined>;
|
|
@@ -109,7 +109,7 @@ export async function buildCascadeArchive(request) {
|
|
|
109
109
|
* stack trace.
|
|
110
110
|
*/
|
|
111
111
|
export function cascadeResultMessage(outcome, labels, where) {
|
|
112
|
-
if (outcome.kind
|
|
112
|
+
if (outcome.kind !== 'exported') {
|
|
113
113
|
return `Markii: could not export this cascade. Open ${labels.diagnosticsSurface} for details.`;
|
|
114
114
|
}
|
|
115
115
|
const name = fileNameOf(outcome.path);
|
|
@@ -149,3 +149,66 @@ export function cascadeDiagnosticLines(outcome) {
|
|
|
149
149
|
}
|
|
150
150
|
return lines;
|
|
151
151
|
}
|
|
152
|
+
/**
|
|
153
|
+
* Exports a note and the notes it links to as one archive: resolves the
|
|
154
|
+
* target through the adapter's picker, builds the archive reading every
|
|
155
|
+
* note through `adapter.readFile`, writes it through `adapter.writeFile`,
|
|
156
|
+
* and sends the diagnostics lines to `adapter.diagnostics`. Resolves
|
|
157
|
+
* `undefined` when the host has no linked-set target or its picker was
|
|
158
|
+
* cancelled. A `targetPath` skips the picker. Never throws.
|
|
159
|
+
*/
|
|
160
|
+
export async function exportLinkedSetViaAdapter(adapter, inputs) {
|
|
161
|
+
const resolveTarget = adapter.exports?.resolveLinkedSetTarget;
|
|
162
|
+
const { suggestedFileName, targetPath, ...archiveRequest } = inputs;
|
|
163
|
+
if (targetPath === undefined && resolveTarget === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
let path;
|
|
167
|
+
let outcome;
|
|
168
|
+
try {
|
|
169
|
+
path =
|
|
170
|
+
targetPath ??
|
|
171
|
+
(await resolveTarget?.call(adapter.exports, suggestedFileName));
|
|
172
|
+
if (path === undefined)
|
|
173
|
+
return undefined;
|
|
174
|
+
const target = path;
|
|
175
|
+
const archive = await buildCascadeArchive({
|
|
176
|
+
...archiveRequest,
|
|
177
|
+
readNote: async (notePath) => {
|
|
178
|
+
try {
|
|
179
|
+
return new TextDecoder().decode(await adapter.readFile(notePath));
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return undefined;
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
if (archive.kind === 'failed') {
|
|
187
|
+
outcome = { kind: 'failed', path: target, reason: archive.reason };
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
await adapter.writeFile(target, archive.bytes);
|
|
191
|
+
outcome = {
|
|
192
|
+
kind: 'exported',
|
|
193
|
+
path: target,
|
|
194
|
+
bytes: archive.bytes.byteLength,
|
|
195
|
+
notes: archive.notes,
|
|
196
|
+
unreadable: archive.unreadable,
|
|
197
|
+
...(archive.truncated !== undefined
|
|
198
|
+
? { truncated: archive.truncated }
|
|
199
|
+
: {}),
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
outcome = {
|
|
205
|
+
kind: 'failed',
|
|
206
|
+
...(path !== undefined ? { path } : {}),
|
|
207
|
+
reason: cascadeReasonOf(error),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
for (const line of cascadeDiagnosticLines(outcome)) {
|
|
211
|
+
adapter.diagnostics(line);
|
|
212
|
+
}
|
|
213
|
+
return outcome;
|
|
214
|
+
}
|
|
@@ -31,10 +31,20 @@ export type HostScenarioAction = {
|
|
|
31
31
|
/** When `true`, this action never reaches `host.run` at all — see this file's top comment on the scripts-disabled scenario. */
|
|
32
32
|
readonly scriptsDisabled?: boolean;
|
|
33
33
|
readonly simulate?: RunSimulation;
|
|
34
|
+
}
|
|
35
|
+
/** Reads what the note's last run left: the values to show on open and the trace. */
|
|
36
|
+
| {
|
|
37
|
+
readonly type: 'lastRun';
|
|
38
|
+
}
|
|
39
|
+
/** Clears the note's stored script grants. */
|
|
40
|
+
| {
|
|
41
|
+
readonly type: 'forgetGrants';
|
|
34
42
|
} | {
|
|
35
43
|
readonly type: 'exportHtml';
|
|
36
44
|
} | {
|
|
37
45
|
readonly type: 'exportAnsi';
|
|
46
|
+
} | {
|
|
47
|
+
readonly type: 'exportLinkedSet';
|
|
38
48
|
} | {
|
|
39
49
|
readonly type: 'installPack';
|
|
40
50
|
/** A directory under the scenario's own folder (e.g. `"files/ana-1.0.0"`), zipped into `.mkp` bytes by this module — never a committed binary. */
|
|
@@ -98,7 +108,7 @@ export interface LoadedHostScenario {
|
|
|
98
108
|
export declare function loadHostScenario(name: string, baseDir?: string): LoadedHostScenario;
|
|
99
109
|
/** 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. */
|
|
100
110
|
export declare function zipArchiveDirectory(archiveDir: string): Uint8Array;
|
|
101
|
-
export type HostScenarioCapability = 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format';
|
|
111
|
+
export type HostScenarioCapability = 'isolate' | 'editor' | 'packs' | 'pack-trust' | 'export-format' | 'linked-set-export';
|
|
102
112
|
/** 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). */
|
|
103
113
|
export declare const ACTION_CAPABILITY: Readonly<Record<HostScenarioAction['type'], HostScenarioCapability | undefined>>;
|
|
104
114
|
/** Whether `adapter` declares the capability `action` needs. Never consults the scenario or `expected.json` — only the adapter under test. */
|
|
@@ -37,14 +37,14 @@
|
|
|
37
37
|
* Every OTHER action runs through the real, unmocked shared behavior code.
|
|
38
38
|
*/
|
|
39
39
|
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
40
|
-
import { dirname } from 'node:path';
|
|
40
|
+
import { dirname, resolve } from 'node:path';
|
|
41
41
|
import { fileURLToPath } from 'node:url';
|
|
42
42
|
import { createHtmlExportEngine } from '../html-engine.js';
|
|
43
43
|
import { usesPackTrust } from './pack-trust-behavior.js';
|
|
44
44
|
import { join, relative, sep } from 'node:path';
|
|
45
|
-
import { zipSync } from 'fflate';
|
|
45
|
+
import { unzipSync, zipSync } from 'fflate';
|
|
46
46
|
import { conformanceDir } from '@markii/core/corpus';
|
|
47
|
-
import {
|
|
47
|
+
import { createMarkiiHostWithSeams, } from './create-host.js';
|
|
48
48
|
import { scriptsDisabledDiagnosticLine, scriptsDisabledNotice, } from './script-execution.js';
|
|
49
49
|
// --- corpus location ----------------------------------------------------
|
|
50
50
|
/**
|
|
@@ -99,8 +99,11 @@ export function zipArchiveDirectory(archiveDir) {
|
|
|
99
99
|
export const ACTION_CAPABILITY = {
|
|
100
100
|
open: undefined,
|
|
101
101
|
run: 'isolate',
|
|
102
|
+
lastRun: undefined,
|
|
103
|
+
forgetGrants: undefined,
|
|
102
104
|
exportHtml: 'export-format',
|
|
103
105
|
exportAnsi: 'export-format',
|
|
106
|
+
exportLinkedSet: 'linked-set-export',
|
|
104
107
|
installPack: 'packs',
|
|
105
108
|
loadPacks: 'packs',
|
|
106
109
|
removePack: 'packs',
|
|
@@ -136,6 +139,9 @@ export function hostDeclaresCapability(adapter, action) {
|
|
|
136
139
|
return adapter.isolate !== undefined;
|
|
137
140
|
if (capability === 'editor')
|
|
138
141
|
return adapter.editor !== undefined;
|
|
142
|
+
if (capability === 'linked-set-export') {
|
|
143
|
+
return adapter.exports?.resolveLinkedSetTarget !== undefined;
|
|
144
|
+
}
|
|
139
145
|
if (capability === 'packs') {
|
|
140
146
|
if (adapter.packs === undefined)
|
|
141
147
|
return false;
|
|
@@ -254,7 +260,7 @@ export async function runHostScenario(adapter, scenario, noteText, scenarioDir,
|
|
|
254
260
|
},
|
|
255
261
|
};
|
|
256
262
|
let currentSimulation;
|
|
257
|
-
const host =
|
|
263
|
+
const host = createMarkiiHostWithSeams(recording, {
|
|
258
264
|
// The runner is Node-only test tooling, so it supplies the real Node
|
|
259
265
|
// implementations of the seams the host layer takes by injection.
|
|
260
266
|
htmlEngine: createHtmlExportEngine(),
|
|
@@ -320,6 +326,18 @@ async function runOneAction(host, adapter, action, ctx) {
|
|
|
320
326
|
trigger: action.trigger,
|
|
321
327
|
});
|
|
322
328
|
}
|
|
329
|
+
case 'lastRun': {
|
|
330
|
+
const last = host.lastRun(ctx.notePath);
|
|
331
|
+
return {
|
|
332
|
+
kind: 'lastRun',
|
|
333
|
+
values: last.values,
|
|
334
|
+
persistedValues: last.persistedValues,
|
|
335
|
+
...(last.trace !== undefined ? { trace: last.trace } : {}),
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
case 'forgetGrants':
|
|
339
|
+
await host.forgetGrants(ctx.notePath);
|
|
340
|
+
return { kind: 'forgotten' };
|
|
323
341
|
case 'exportHtml':
|
|
324
342
|
case 'exportAnsi': {
|
|
325
343
|
const outcome = await host.exportNote({
|
|
@@ -329,6 +347,24 @@ async function runOneAction(host, adapter, action, ctx) {
|
|
|
329
347
|
});
|
|
330
348
|
return augmentExportOutcome(outcome, ctx.writtenFiles);
|
|
331
349
|
}
|
|
350
|
+
case 'exportLinkedSet': {
|
|
351
|
+
const outcome = await host.exportLinkedSet({
|
|
352
|
+
rootPath: ctx.notePath,
|
|
353
|
+
// A markdown link to a note file that exists beside the linking
|
|
354
|
+
// note: the same rule for every host, so the archive's contents
|
|
355
|
+
// do not depend on which host runs the scenario.
|
|
356
|
+
resolveLink: (link, fromNotePath) => {
|
|
357
|
+
if (link.kind !== 'markdown')
|
|
358
|
+
return undefined;
|
|
359
|
+
const target = resolve(dirname(fromNotePath), link.path);
|
|
360
|
+
return target.endsWith('.md') && existsSync(target)
|
|
361
|
+
? target
|
|
362
|
+
: undefined;
|
|
363
|
+
},
|
|
364
|
+
readValues: () => ({}),
|
|
365
|
+
});
|
|
366
|
+
return augmentLinkedSetOutcome(outcome, ctx.writtenFiles);
|
|
367
|
+
}
|
|
332
368
|
case 'installPack': {
|
|
333
369
|
const archiveBytes = zipArchiveDirectory(join(ctx.scenarioDir, action.archive));
|
|
334
370
|
return host.installPack({ archiveBytes, archivePath: action.archive });
|
|
@@ -361,6 +397,13 @@ async function runOneAction(host, adapter, action, ctx) {
|
|
|
361
397
|
return host.insertComponent({ name: action.name });
|
|
362
398
|
}
|
|
363
399
|
}
|
|
400
|
+
function augmentLinkedSetOutcome(outcome, writtenFiles) {
|
|
401
|
+
if (outcome.kind !== 'exported')
|
|
402
|
+
return outcome;
|
|
403
|
+
const bytes = writtenFiles.get(outcome.path);
|
|
404
|
+
const entries = bytes === undefined ? undefined : Object.keys(unzipSync(bytes)).sort();
|
|
405
|
+
return { ...outcome, noteCount: outcome.notes.length, entries };
|
|
406
|
+
}
|
|
364
407
|
function augmentExportOutcome(outcome, writtenFiles) {
|
|
365
408
|
if (outcome.kind !== 'exported')
|
|
366
409
|
return outcome;
|
|
@@ -446,6 +489,30 @@ export function outcomeMismatches(actual, expected) {
|
|
|
446
489
|
}
|
|
447
490
|
break;
|
|
448
491
|
}
|
|
492
|
+
case 'lastRun': {
|
|
493
|
+
const wantValues = expected['values'] ?? {};
|
|
494
|
+
const gotValues = landedValues(actualRecord['values']);
|
|
495
|
+
if (JSON.stringify(gotValues) !== JSON.stringify(wantValues)) {
|
|
496
|
+
mismatches.push(`values: expected ${JSON.stringify(wantValues)}, got ${JSON.stringify(gotValues)}`);
|
|
497
|
+
}
|
|
498
|
+
if (expected['valueStatuses'] !== undefined) {
|
|
499
|
+
const statuses = {};
|
|
500
|
+
const raw = (actualRecord['values'] ?? {});
|
|
501
|
+
for (const [name, entry] of Object.entries(raw)) {
|
|
502
|
+
statuses[name] = entry.status;
|
|
503
|
+
}
|
|
504
|
+
if (JSON.stringify(statuses) !== JSON.stringify(expected['valueStatuses'])) {
|
|
505
|
+
mismatches.push(`value statuses: expected ${JSON.stringify(expected['valueStatuses'])}, got ${JSON.stringify(statuses)}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
if (expected['traceOk'] !== undefined) {
|
|
509
|
+
const trace = actualRecord['trace'];
|
|
510
|
+
if (trace?.ok !== expected['traceOk']) {
|
|
511
|
+
mismatches.push(`trace.ok: expected ${String(expected['traceOk'])}, got ${String(trace?.ok)}`);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
449
516
|
case 'exported': {
|
|
450
517
|
if (expected['valueCount'] !== undefined &&
|
|
451
518
|
actualRecord['valueCount'] !== expected['valueCount']) {
|
|
@@ -457,6 +524,15 @@ export function outcomeMismatches(actual, expected) {
|
|
|
457
524
|
mismatches.push(`render engine: expected "${String(expected['renderEngine'])}", got "${String(render?.engine)}"`);
|
|
458
525
|
}
|
|
459
526
|
}
|
|
527
|
+
if (expected['noteCount'] !== undefined &&
|
|
528
|
+
actualRecord['noteCount'] !== expected['noteCount']) {
|
|
529
|
+
mismatches.push(`noteCount: expected ${String(expected['noteCount'])}, got ${String(actualRecord['noteCount'])}`);
|
|
530
|
+
}
|
|
531
|
+
if (expected['entries'] !== undefined &&
|
|
532
|
+
JSON.stringify(actualRecord['entries']) !==
|
|
533
|
+
JSON.stringify(expected['entries'])) {
|
|
534
|
+
mismatches.push(`archive entries: expected ${JSON.stringify(expected['entries'])}, got ${JSON.stringify(actualRecord['entries'])}`);
|
|
535
|
+
}
|
|
460
536
|
const content = String(actualRecord['content'] ?? '');
|
|
461
537
|
for (const needle of expected['contains'] ?? []) {
|
|
462
538
|
if (!content.includes(needle)) {
|