@markii/host 0.17.0-next.5 → 0.17.0
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/dist/browser-worker.classic.js +43 -43
- package/dist/browser-worker.js +43 -43
- 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 +11 -4
- package/dist/conformance.d.ts +1 -1
- package/dist/conformance.js +1 -1
- package/dist/host/adapter.d.ts +21 -7
- package/dist/host/cascade-export-behavior.d.ts +3 -1
- package/dist/host/cascade-export-behavior.js +7 -4
- package/dist/host/conformance-runner.d.ts +8 -0
- package/dist/host/conformance-runner.js +40 -2
- package/dist/host/create-host.d.ts +231 -62
- package/dist/host/create-host.js +204 -98
- 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 +22 -1
- package/dist/host/run-behavior.d.ts +21 -34
- package/dist/host/run-behavior.js +23 -5
- package/dist/index.d.ts +5 -10
- package/dist/index.js +0 -1
- package/dist/internal.d.ts +9 -2
- package/dist/internal.js +3 -2
- package/dist/node-worker.js +3 -1
- package/dist/node.d.ts +1 -2
- package/dist/node.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,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,6 +47,8 @@ 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`) |
|
|
52
54
|
| `exportLinkedSet` | `linked-set-export` (a `resolveLinkedSetTarget`) |
|
|
@@ -137,6 +139,9 @@ coarse, matching each `kind`:
|
|
|
137
139
|
(`{name: value}`); `failureKinds` compares the closed `FailureKind`
|
|
138
140
|
taxonomy only, never a raw message or a timing; an optional `traceOk`
|
|
139
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.
|
|
140
145
|
- `exported`: `contains`/`excludes` are substrings checked against the
|
|
141
146
|
written file's content, never the whole document, the render corpora
|
|
142
147
|
already pin bytes. `valueCount` is how many last-run values were baked
|
|
@@ -147,7 +152,8 @@ coarse, matching each `kind`:
|
|
|
147
152
|
of the folder the pack was unzipped into.
|
|
148
153
|
- `declined`: `step` (`'consent'`/`'replace'`, or absent for the
|
|
149
154
|
scripts-disabled synthetic outcome) and `reasonContains`, a substring of
|
|
150
|
-
the user-facing reason.
|
|
155
|
+
the user-facing reason. A user's "no" to a prompt and a cancelled picker
|
|
156
|
+
are both `declined`.
|
|
151
157
|
- `listed`: `enabled` and `notEnabled`, namespace lists, order-independent.
|
|
152
158
|
- `enabled`, `disabled`, `placed`, `removed`: `namespace`.
|
|
153
159
|
- `loaded`: `namespaces`, order-independent.
|
|
@@ -159,8 +165,8 @@ coarse, matching each `kind`:
|
|
|
159
165
|
accepted container item.
|
|
160
166
|
- `hover`: `contains`, a substring of the hover text. Optional `start` and
|
|
161
167
|
`end` compare the hovered name's columns.
|
|
162
|
-
- every other `kind` (`opened`, `none`, `not-found`, `failed`, `
|
|
163
|
-
`reserved`, `
|
|
168
|
+
- every other `kind` (`opened`, `none`, `not-found`, `failed`, `invalid`,
|
|
169
|
+
`reserved`, `forgotten`, `inserted`): `kind` equality is the whole check.
|
|
164
170
|
|
|
165
171
|
An action whose capability the adapter under test does not declare is
|
|
166
172
|
never diffed against `expected.json` at all (see "Capability handling"
|
|
@@ -200,7 +206,7 @@ run the whole corpus honestly: its result records say `unsupported`
|
|
|
200
206
|
where it genuinely offers nothing, which is the truth and is visible,
|
|
201
207
|
rather than a skipped test or a faked empty answer.
|
|
202
208
|
|
|
203
|
-
## The
|
|
209
|
+
## The scenarios
|
|
204
210
|
|
|
205
211
|
| id | what it proves |
|
|
206
212
|
|---|---|
|
|
@@ -221,6 +227,7 @@ rather than a skipped test or a faked empty answer.
|
|
|
221
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 |
|
|
222
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` |
|
|
223
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 |
|
|
224
231
|
|
|
225
232
|
The capabilities an adapter declares decide which scenarios report
|
|
226
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';
|
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,16 +155,30 @@ 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>;
|
|
164
179
|
/**
|
|
165
180
|
* Names the zip archive of a linked-set export, or resolves `undefined`
|
|
166
|
-
* when the user cancels.
|
|
167
|
-
* linked-set export; a host can offer single-note export without it
|
|
181
|
+
* when the user cancels. A host can offer single-note export without it
|
|
168
182
|
* (the command line tool does), so it is a member of its own rather than
|
|
169
183
|
* a `HostExportFormat`.
|
|
170
184
|
*/
|
|
@@ -124,6 +124,8 @@ export declare function cascadeDiagnosticLines(outcome: CascadeExportOutcome): s
|
|
|
124
124
|
export interface LinkedSetExportInputs extends Omit<CascadeArchiveRequest, 'readNote'> {
|
|
125
125
|
/** The suggested archive file name handed to the host's picker. */
|
|
126
126
|
readonly suggestedFileName: string;
|
|
127
|
+
/** Where to write the archive. When present the adapter's `resolveLinkedSetTarget` is not asked. */
|
|
128
|
+
readonly targetPath?: string;
|
|
127
129
|
}
|
|
128
130
|
/**
|
|
129
131
|
* Exports a note and the notes it links to as one archive: resolves the
|
|
@@ -131,6 +133,6 @@ export interface LinkedSetExportInputs extends Omit<CascadeArchiveRequest, 'read
|
|
|
131
133
|
* note through `adapter.readFile`, writes it through `adapter.writeFile`,
|
|
132
134
|
* and sends the diagnostics lines to `adapter.diagnostics`. Resolves
|
|
133
135
|
* `undefined` when the host has no linked-set target or its picker was
|
|
134
|
-
* cancelled. Never throws.
|
|
136
|
+
* cancelled. A `targetPath` skips the picker. Never throws.
|
|
135
137
|
*/
|
|
136
138
|
export declare function exportLinkedSetViaAdapter(adapter: HostAdapter, inputs: LinkedSetExportInputs): Promise<CascadeExportOutcome | undefined>;
|
|
@@ -155,17 +155,20 @@ export function cascadeDiagnosticLines(outcome) {
|
|
|
155
155
|
* note through `adapter.readFile`, writes it through `adapter.writeFile`,
|
|
156
156
|
* and sends the diagnostics lines to `adapter.diagnostics`. Resolves
|
|
157
157
|
* `undefined` when the host has no linked-set target or its picker was
|
|
158
|
-
* cancelled. Never throws.
|
|
158
|
+
* cancelled. A `targetPath` skips the picker. Never throws.
|
|
159
159
|
*/
|
|
160
160
|
export async function exportLinkedSetViaAdapter(adapter, inputs) {
|
|
161
161
|
const resolveTarget = adapter.exports?.resolveLinkedSetTarget;
|
|
162
|
-
|
|
162
|
+
const { suggestedFileName, targetPath, ...archiveRequest } = inputs;
|
|
163
|
+
if (targetPath === undefined && resolveTarget === undefined) {
|
|
163
164
|
return undefined;
|
|
164
|
-
|
|
165
|
+
}
|
|
165
166
|
let path;
|
|
166
167
|
let outcome;
|
|
167
168
|
try {
|
|
168
|
-
path =
|
|
169
|
+
path =
|
|
170
|
+
targetPath ??
|
|
171
|
+
(await resolveTarget?.call(adapter.exports, suggestedFileName));
|
|
169
172
|
if (path === undefined)
|
|
170
173
|
return undefined;
|
|
171
174
|
const target = path;
|
|
@@ -31,6 +31,14 @@ 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
|
} | {
|
|
@@ -44,7 +44,7 @@ import { usesPackTrust } from './pack-trust-behavior.js';
|
|
|
44
44
|
import { join, relative, sep } from 'node:path';
|
|
45
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,6 +99,8 @@ 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',
|
|
104
106
|
exportLinkedSet: 'linked-set-export',
|
|
@@ -258,7 +260,7 @@ export async function runHostScenario(adapter, scenario, noteText, scenarioDir,
|
|
|
258
260
|
},
|
|
259
261
|
};
|
|
260
262
|
let currentSimulation;
|
|
261
|
-
const host =
|
|
263
|
+
const host = createMarkiiHostWithSeams(recording, {
|
|
262
264
|
// The runner is Node-only test tooling, so it supplies the real Node
|
|
263
265
|
// implementations of the seams the host layer takes by injection.
|
|
264
266
|
htmlEngine: createHtmlExportEngine(),
|
|
@@ -324,6 +326,18 @@ async function runOneAction(host, adapter, action, ctx) {
|
|
|
324
326
|
trigger: action.trigger,
|
|
325
327
|
});
|
|
326
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' };
|
|
327
341
|
case 'exportHtml':
|
|
328
342
|
case 'exportAnsi': {
|
|
329
343
|
const outcome = await host.exportNote({
|
|
@@ -475,6 +489,30 @@ export function outcomeMismatches(actual, expected) {
|
|
|
475
489
|
}
|
|
476
490
|
break;
|
|
477
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
|
+
}
|
|
478
516
|
case 'exported': {
|
|
479
517
|
if (expected['valueCount'] !== undefined &&
|
|
480
518
|
actualRecord['valueCount'] !== expected['valueCount']) {
|