@velarscript/desktop 0.10.1
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/LICENSE +201 -0
- package/README.md +247 -0
- package/dist/build.d.ts +53 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +351 -0
- package/dist/build.js.map +1 -0
- package/dist/compiler.d.ts +4 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +1967 -0
- package/dist/compiler.js.map +1 -0
- package/dist/config.d.ts +33 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +140 -0
- package/dist/config.js.map +1 -0
- package/dist/host.d.ts +3 -0
- package/dist/host.d.ts.map +1 -0
- package/dist/host.js +71 -0
- package/dist/host.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/package-host.d.ts +3 -0
- package/dist/package-host.d.ts.map +1 -0
- package/dist/package-host.js +20 -0
- package/dist/package-host.js.map +1 -0
- package/dist/test-runtime.d.ts +16 -0
- package/dist/test-runtime.d.ts.map +1 -0
- package/dist/test-runtime.js +475 -0
- package/dist/test-runtime.js.map +1 -0
- package/dist/worker.js +546 -0
- package/native/macos/VelarDesktopHost.swift +1258 -0
- package/native/macos/VelarScript.icns +0 -0
- package/native/macos/VelarTerminalHost.swift +203 -0
- package/native/node/project-transactions.js +157 -0
- package/native/node/worker.js +2220 -0
- package/package.json +63 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Owns the one pre-navigation choice a Desktop browser test may make. Each
|
|
3
|
+
* test receives a fresh controller, and the choice is sealed when its first
|
|
4
|
+
* `browser.open()` requests the application init script.
|
|
5
|
+
*/
|
|
6
|
+
export function desktopBrowserTestController(config) {
|
|
7
|
+
let platform = "test";
|
|
8
|
+
let opened = false;
|
|
9
|
+
return Object.freeze({
|
|
10
|
+
initScript() {
|
|
11
|
+
opened = true;
|
|
12
|
+
return desktopBrowserTestInitScript(config, platform);
|
|
13
|
+
},
|
|
14
|
+
invoke(capability, operation, args) {
|
|
15
|
+
if (capability !== "desktop-test" || operation !== "setPlatform")
|
|
16
|
+
return { handled: false };
|
|
17
|
+
if (opened)
|
|
18
|
+
throw new Error("Desktop test platform must be set before the first browser.open()");
|
|
19
|
+
if (args.length !== 1 || args[0] !== "macos" && args[0] !== "test") {
|
|
20
|
+
throw new TypeError("Desktop test platform must be DesktopPlatform.macos or DesktopPlatform.test");
|
|
21
|
+
}
|
|
22
|
+
platform = args[0];
|
|
23
|
+
return { handled: true, value: null };
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Creates the deterministic capability host used by `velar test --browser`.
|
|
29
|
+
* It is intentionally an in-memory filesystem, not a browser polyfill for the
|
|
30
|
+
* operating system. The native worker has a separate integration suite.
|
|
31
|
+
*/
|
|
32
|
+
export function desktopBrowserTestInitScript(config, platform = "test") {
|
|
33
|
+
const files = JSON.stringify(config.permissions.files);
|
|
34
|
+
const processes = JSON.stringify(config.permissions.processes);
|
|
35
|
+
return String.raw `
|
|
36
|
+
(() => {
|
|
37
|
+
"use strict";
|
|
38
|
+
const protocol = Symbol.for("velar.desktop.bridge.v1");
|
|
39
|
+
const projectRoot = "/velar-test/project";
|
|
40
|
+
const appDataRoot = "/velar-test/app-data";
|
|
41
|
+
let selectedProjectRoot = null;
|
|
42
|
+
const grants = new Set(${files});
|
|
43
|
+
const processGrants = new Set(${processes});
|
|
44
|
+
// A permission is not a value. Tests start with an absent environment so a
|
|
45
|
+
// granted production setting cannot accidentally activate external behavior.
|
|
46
|
+
const environment = Object.freeze({});
|
|
47
|
+
const nodes = new Map();
|
|
48
|
+
const processHandles = new Map();
|
|
49
|
+
const fileWatchers = new Map();
|
|
50
|
+
const projectChanges = new Map();
|
|
51
|
+
const projectChangeHandles = new Map();
|
|
52
|
+
let nextProcessHandle = 1;
|
|
53
|
+
let nextFileWatcherHandle = 1;
|
|
54
|
+
let nextProjectChangeHandle = 1;
|
|
55
|
+
let nextProjectChangeSequence = 1;
|
|
56
|
+
const now = 0;
|
|
57
|
+
const maxFileBytes = 16 * 1024 * 1024;
|
|
58
|
+
const maxListItems = 100000;
|
|
59
|
+
const maxListTextUnits = 2 * 1024 * 1024;
|
|
60
|
+
const maxWatchPaths = 4096;
|
|
61
|
+
const maxWatchTextUnits = 2 * 1024 * 1024;
|
|
62
|
+
|
|
63
|
+
function projectChangeLifecycle(value) {
|
|
64
|
+
if (value === "prepared" || value === "amended" || value === "validated" || value === "applied" || value === "discarded") return value;
|
|
65
|
+
if (value === "validationFailed" || value === "validation_failed") return "validation_failed";
|
|
66
|
+
if (value === "rolledBack" || value === "rolled_back") return "rolled_back";
|
|
67
|
+
throw new TypeError("Desktop test project change lifecycle is invalid");
|
|
68
|
+
}
|
|
69
|
+
function projectChangeId(value) {
|
|
70
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 512 || value.includes("\0")) {
|
|
71
|
+
throw new TypeError("Desktop test project change id must be bounded text");
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
function projectChangeDiff(value) {
|
|
76
|
+
if (typeof value !== "string" || new TextEncoder().encode(value).byteLength > 16 * 1024 * 1024) {
|
|
77
|
+
throw new RangeError("Desktop test project change diff cannot exceed 16 MiB");
|
|
78
|
+
}
|
|
79
|
+
return value;
|
|
80
|
+
}
|
|
81
|
+
function changeLines(diff) {
|
|
82
|
+
if (diff.length === 0) return 0;
|
|
83
|
+
return diff.split("\n").filter(line => line.startsWith("+") || line.startsWith("-")).length;
|
|
84
|
+
}
|
|
85
|
+
function notifyProjectChanges(change) {
|
|
86
|
+
for (const handle of projectChangeHandles.values()) {
|
|
87
|
+
if (handle.closed) continue;
|
|
88
|
+
if (!handle.queue.has(change.transactionId) && handle.queue.size >= 100) {
|
|
89
|
+
handle.queue.clear();
|
|
90
|
+
handle.rescan = true;
|
|
91
|
+
} else if (!handle.rescan) handle.queue.set(change.transactionId, change);
|
|
92
|
+
if (handle.pending) {
|
|
93
|
+
const resolveNext = handle.pending;
|
|
94
|
+
handle.pending = null;
|
|
95
|
+
const changes = [...handle.queue.values()];
|
|
96
|
+
handle.queue.clear();
|
|
97
|
+
const rescan = handle.rescan;
|
|
98
|
+
handle.rescan = false;
|
|
99
|
+
resolveNext({changes, rescan});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function recordProjectChange(transactionId, lifecycle, diff, previous = null) {
|
|
104
|
+
const sequence = nextProjectChangeSequence++;
|
|
105
|
+
const changedLines = changeLines(diff);
|
|
106
|
+
const change = Object.freeze({
|
|
107
|
+
transactionId,
|
|
108
|
+
sequence,
|
|
109
|
+
lifecycle,
|
|
110
|
+
reason: null,
|
|
111
|
+
intents: Object.freeze([Object.freeze({type: "replace_text", path: "src/main.vel", from: null, to: null, targetId: null, reason: "desktop browser test"})]),
|
|
112
|
+
patches: Object.freeze([Object.freeze({
|
|
113
|
+
patchId: "desktop-test:" + transactionId,
|
|
114
|
+
strategyId: "desktop-test",
|
|
115
|
+
path: "src/main.vel",
|
|
116
|
+
baseRevision: null,
|
|
117
|
+
diff,
|
|
118
|
+
changedLines,
|
|
119
|
+
risk: "low",
|
|
120
|
+
operation: "replace_text",
|
|
121
|
+
})]),
|
|
122
|
+
changedFiles: Object.freeze(["src/main.vel"]),
|
|
123
|
+
diff,
|
|
124
|
+
changedLines,
|
|
125
|
+
risk: "low",
|
|
126
|
+
revisions: Object.freeze([Object.freeze({path: "src/main.vel", before: previous ? previous.revisions[0].before : "desktop-test:before", after: "desktop-test:" + sequence})]),
|
|
127
|
+
createdAt: previous ? previous.createdAt : sequence,
|
|
128
|
+
updatedAt: sequence,
|
|
129
|
+
appliedAt: lifecycle === "applied" ? sequence : previous?.appliedAt ?? null,
|
|
130
|
+
});
|
|
131
|
+
projectChanges.set(transactionId, change);
|
|
132
|
+
notifyProjectChanges(change);
|
|
133
|
+
return change;
|
|
134
|
+
}
|
|
135
|
+
async function projectChangeCapability(operation, args) {
|
|
136
|
+
if (!grants.has("project")) throw new Error("Desktop test project changes require the project file grant");
|
|
137
|
+
if (operation === "start") {
|
|
138
|
+
if (args.length !== 0) throw new TypeError("Desktop test projectChanges start arguments are invalid");
|
|
139
|
+
const handle = nextProjectChangeHandle++;
|
|
140
|
+
projectChangeHandles.set(handle, {queue: new Map(), rescan: false, pending: null, closed: false});
|
|
141
|
+
return handle;
|
|
142
|
+
}
|
|
143
|
+
const handle = projectChangeHandles.get(args[0]);
|
|
144
|
+
if (!handle || handle.closed) throw new Error("Desktop test ProjectChanges handle is unknown or already released");
|
|
145
|
+
if (operation === "list") {
|
|
146
|
+
const limit = args[1];
|
|
147
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) throw new RangeError("Desktop test ProjectChanges list limit is invalid");
|
|
148
|
+
const values = [...projectChanges.values()].sort((left, right) => right.sequence - left.sequence);
|
|
149
|
+
return {changes: values.slice(0, limit), truncated: values.length > limit};
|
|
150
|
+
}
|
|
151
|
+
if (operation === "get") return projectChanges.get(projectChangeId(args[1])) ?? null;
|
|
152
|
+
if (operation === "subscribe") {
|
|
153
|
+
if (handle.pending) throw new Error("Desktop test ProjectChanges subscribe already has an active pull");
|
|
154
|
+
if (handle.rescan || handle.queue.size > 0) {
|
|
155
|
+
const changes = [...handle.queue.values()];
|
|
156
|
+
handle.queue.clear();
|
|
157
|
+
const rescan = handle.rescan;
|
|
158
|
+
handle.rescan = false;
|
|
159
|
+
return {changes, rescan};
|
|
160
|
+
}
|
|
161
|
+
return new Promise(resolveNext => { handle.pending = resolveNext; });
|
|
162
|
+
}
|
|
163
|
+
if (operation === "apply" || operation === "rollback") {
|
|
164
|
+
const transactionId = projectChangeId(args[1]);
|
|
165
|
+
const current = projectChanges.get(transactionId);
|
|
166
|
+
if (!current) throw new Error("Desktop test project transaction is unknown");
|
|
167
|
+
if (operation === "apply" && current.lifecycle === "applied") throw new Error("Desktop test project transaction is already applied");
|
|
168
|
+
if (operation === "rollback" && current.lifecycle !== "applied") throw new Error("Desktop test project transaction is not applied");
|
|
169
|
+
return recordProjectChange(transactionId, operation === "apply" ? "applied" : "rolled_back", current.diff, current);
|
|
170
|
+
}
|
|
171
|
+
if (operation === "close") {
|
|
172
|
+
handle.closed = true;
|
|
173
|
+
projectChangeHandles.delete(args[0]);
|
|
174
|
+
if (handle.pending) { const resolveNext = handle.pending; handle.pending = null; resolveNext(null); }
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
throw new Error("Unsupported Desktop test project change operation '" + operation + "'");
|
|
178
|
+
}
|
|
179
|
+
async function projectChangeTest(operation, args) {
|
|
180
|
+
if (operation !== "seed" || args.length !== 3) throw new TypeError("Desktop test project change seed arguments are invalid");
|
|
181
|
+
if (!grants.has("project")) throw new Error("Desktop test project change seed requires the project file grant");
|
|
182
|
+
const transactionId = projectChangeId(args[0]);
|
|
183
|
+
recordProjectChange(transactionId, projectChangeLifecycle(args[1]), projectChangeDiff(args[2]), projectChanges.get(transactionId) ?? null);
|
|
184
|
+
return null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function watchBatch(watcher) {
|
|
188
|
+
if (watcher.rescan) {
|
|
189
|
+
watcher.rescan = false;
|
|
190
|
+
watcher.paths.clear();
|
|
191
|
+
watcher.units = 0;
|
|
192
|
+
return {paths: [], rescan: true};
|
|
193
|
+
}
|
|
194
|
+
const paths = [...watcher.paths].sort();
|
|
195
|
+
watcher.paths.clear();
|
|
196
|
+
watcher.units = 0;
|
|
197
|
+
return {paths, rescan: false};
|
|
198
|
+
}
|
|
199
|
+
function settleWatch(watcher) {
|
|
200
|
+
if (!watcher.pending || watcher.scheduled || !watcher.rescan && watcher.paths.size === 0) return;
|
|
201
|
+
watcher.scheduled = true;
|
|
202
|
+
Promise.resolve().then(() => {
|
|
203
|
+
watcher.scheduled = false;
|
|
204
|
+
if (!watcher.pending || watcher.closed) return;
|
|
205
|
+
const resolveNext = watcher.pending;
|
|
206
|
+
watcher.pending = null;
|
|
207
|
+
resolveNext(watchBatch(watcher));
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function notifyWatchers(path) {
|
|
211
|
+
for (const watcher of fileWatchers.values()) {
|
|
212
|
+
if (watcher.closed || path !== watcher.root && !(watcher.recursive ? contained(watcher.root, path) : parent(path) === watcher.root)) continue;
|
|
213
|
+
if (!watcher.paths.has(path)) {
|
|
214
|
+
if (watcher.paths.size >= maxWatchPaths || watcher.units + path.length > maxWatchTextUnits) {
|
|
215
|
+
watcher.rescan = true;
|
|
216
|
+
watcher.paths.clear();
|
|
217
|
+
watcher.units = 0;
|
|
218
|
+
} else {
|
|
219
|
+
watcher.paths.add(path);
|
|
220
|
+
watcher.units += path.length;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
settleWatch(watcher);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function normalize(value) {
|
|
228
|
+
if (typeof value !== "string" || value.length === 0 || value.length > 4096 || value.includes("\0")) {
|
|
229
|
+
throw new TypeError("Desktop test paths must be bounded non-empty text");
|
|
230
|
+
}
|
|
231
|
+
const absolute = value.startsWith("/");
|
|
232
|
+
const output = [];
|
|
233
|
+
for (const part of value.split("/")) {
|
|
234
|
+
if (!part || part === ".") continue;
|
|
235
|
+
if (part === "..") {
|
|
236
|
+
if (output.length) output.pop();
|
|
237
|
+
else if (!absolute) output.push("..");
|
|
238
|
+
} else output.push(part);
|
|
239
|
+
}
|
|
240
|
+
return (absolute ? "/" : "") + output.join("/") || (absolute ? "/" : ".");
|
|
241
|
+
}
|
|
242
|
+
function contained(root, target) { return target === root || target.startsWith(root + "/"); }
|
|
243
|
+
function authorized(value) {
|
|
244
|
+
let path = normalize(value);
|
|
245
|
+
if (!path.startsWith("/")) {
|
|
246
|
+
const base = grants.has("project") ? projectRoot : grants.has("app-data") ? appDataRoot : null;
|
|
247
|
+
if (!base) throw new Error("Desktop test application has no granted filesystem scope");
|
|
248
|
+
path = normalize(base + "/" + path);
|
|
249
|
+
}
|
|
250
|
+
if (grants.has("project") && contained(projectRoot, path)) return path;
|
|
251
|
+
if (grants.has("app-data") && contained(appDataRoot, path)) return path;
|
|
252
|
+
throw new Error("Desktop test path is outside granted file roots");
|
|
253
|
+
}
|
|
254
|
+
function parent(path) { const index = path.lastIndexOf("/"); return index <= 0 ? "/" : path.slice(0, index); }
|
|
255
|
+
function name(path) { return path === "/" ? "" : path.slice(path.lastIndexOf("/") + 1); }
|
|
256
|
+
function directory(path) { nodes.set(path, Object.freeze({ kind: "directory", body: "", modifiedAt: now })); notifyWatchers(path); }
|
|
257
|
+
function makeDirectories(value) {
|
|
258
|
+
const path = authorized(value);
|
|
259
|
+
const parts = path.split("/").filter(Boolean);
|
|
260
|
+
let current = "";
|
|
261
|
+
for (const part of parts) {
|
|
262
|
+
current += "/" + part;
|
|
263
|
+
const existing = nodes.get(current);
|
|
264
|
+
if (existing && existing.kind !== "directory") throw new Error("Desktop test directory conflicts with a file");
|
|
265
|
+
if (!existing) directory(current);
|
|
266
|
+
}
|
|
267
|
+
return path;
|
|
268
|
+
}
|
|
269
|
+
function file(path, body) {
|
|
270
|
+
path = authorized(path);
|
|
271
|
+
if (!nodes.has(parent(path))) throw new Error("Desktop test parent directory does not exist");
|
|
272
|
+
if (typeof body !== "string") throw new TypeError("Desktop test file content must be text");
|
|
273
|
+
if (new TextEncoder().encode(body).byteLength > maxFileBytes) throw new RangeError("Desktop test file content cannot exceed 16 MiB");
|
|
274
|
+
if (nodes.get(path)?.kind === "directory") throw new TypeError("Desktop test file operation requires a file path");
|
|
275
|
+
nodes.set(path, Object.freeze({ kind: "file", body, modifiedAt: now }));
|
|
276
|
+
notifyWatchers(path);
|
|
277
|
+
}
|
|
278
|
+
function existing(value) {
|
|
279
|
+
const path = authorized(value);
|
|
280
|
+
const node = nodes.get(path);
|
|
281
|
+
if (!node) throw new Error("Desktop test path does not exist");
|
|
282
|
+
return { path, node };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (grants.has("project")) {
|
|
286
|
+
makeDirectories(projectRoot);
|
|
287
|
+
file(projectRoot + "/README.md", "# Velar Desktop test project\n");
|
|
288
|
+
makeDirectories(projectRoot + "/src");
|
|
289
|
+
file(projectRoot + "/src/main.vel", "import {App} from \"./app.vel\"\n\nmount(<App />, \"#app\")\n");
|
|
290
|
+
}
|
|
291
|
+
if (grants.has("app-data")) makeDirectories(appDataRoot);
|
|
292
|
+
|
|
293
|
+
async function fs(operation, args) {
|
|
294
|
+
if (operation === "watchStart") {
|
|
295
|
+
if (args.length !== 2 || typeof args[1] !== "boolean") throw new TypeError("watchStart arguments are invalid");
|
|
296
|
+
if (fileWatchers.size >= 128) throw new RangeError("Desktop test host cannot own more than 128 file watchers");
|
|
297
|
+
const {path, node} = existing(args[0]);
|
|
298
|
+
if (node.kind !== "directory" && node.kind !== "file") throw new TypeError("watchFiles requires a file or directory path");
|
|
299
|
+
if (args[1] && node.kind !== "directory") throw new TypeError("recursive watchFiles requires a directory path");
|
|
300
|
+
const handle = nextFileWatcherHandle++;
|
|
301
|
+
fileWatchers.set(handle, {root: path, recursive: args[1], paths: new Set(), units: 0, rescan: false, pending: null, scheduled: false, closed: false});
|
|
302
|
+
return handle;
|
|
303
|
+
}
|
|
304
|
+
if (operation === "watchNext") {
|
|
305
|
+
const watcher = fileWatchers.get(args[0]);
|
|
306
|
+
if (!watcher) throw new Error("Desktop test file watcher handle is unknown or already released");
|
|
307
|
+
if (watcher.pending) throw new Error("FileWatcher.next already has an active pull");
|
|
308
|
+
if (watcher.rescan || watcher.paths.size > 0) return watchBatch(watcher);
|
|
309
|
+
return new Promise(resolveNext => { watcher.pending = resolveNext; });
|
|
310
|
+
}
|
|
311
|
+
if (operation === "watchClose") {
|
|
312
|
+
const watcher = fileWatchers.get(args[0]);
|
|
313
|
+
if (!watcher) return false;
|
|
314
|
+
watcher.closed = true;
|
|
315
|
+
fileWatchers.delete(args[0]);
|
|
316
|
+
if (watcher.pending) { const resolveNext = watcher.pending; watcher.pending = null; resolveNext(null); }
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
if (operation === "canonical") return existing(args[0]).path;
|
|
320
|
+
if (operation === "exists") { const path = authorized(args[0]); return nodes.has(path); }
|
|
321
|
+
if (operation === "makeDirectory") { makeDirectories(args[0]); return null; }
|
|
322
|
+
if (operation === "readText") {
|
|
323
|
+
const { node } = existing(args[0]);
|
|
324
|
+
if (node.kind !== "file") throw new Error("readText requires a file path");
|
|
325
|
+
const maximum = args[1] ?? maxFileBytes;
|
|
326
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > maxFileBytes) throw new RangeError("readText maxBytes is outside its supported bounds");
|
|
327
|
+
if (new TextEncoder().encode(node.body).byteLength > maximum) throw new RangeError("readText file exceeds maxBytes");
|
|
328
|
+
return node.body;
|
|
329
|
+
}
|
|
330
|
+
if (operation === "createText") {
|
|
331
|
+
const path = authorized(args[0]);
|
|
332
|
+
if (nodes.has(path)) throw new Error("createText target already exists");
|
|
333
|
+
file(path, args[1]);
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
if (operation === "replaceTextIfMatches") {
|
|
337
|
+
const path = authorized(args[0]);
|
|
338
|
+
const current = nodes.get(path);
|
|
339
|
+
if (!current || current.kind !== "file") throw new Error("replaceTextIfMatches requires a file path");
|
|
340
|
+
if (typeof args[1] !== "string" || typeof args[2] !== "string") throw new TypeError("replaceTextIfMatches requires text");
|
|
341
|
+
if (current.body !== args[1]) return false;
|
|
342
|
+
file(path, args[2]);
|
|
343
|
+
return true;
|
|
344
|
+
}
|
|
345
|
+
if (operation === "writeText") { file(args[0], args[1]); return null; }
|
|
346
|
+
if (operation === "appendText") {
|
|
347
|
+
const path = authorized(args[0]);
|
|
348
|
+
const current = nodes.get(path);
|
|
349
|
+
if (current && current.kind !== "file") throw new TypeError("appendText requires a file path");
|
|
350
|
+
if (typeof args[1] !== "string") throw new TypeError("appendText requires text");
|
|
351
|
+
file(path, (current?.kind === "file" ? current.body : "") + args[1]);
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
if (operation === "list") {
|
|
355
|
+
const { path, node } = existing(args[0]);
|
|
356
|
+
if (node.kind !== "directory") throw new Error("list requires a directory path");
|
|
357
|
+
const prefix = path + "/";
|
|
358
|
+
const names = [...nodes.keys()].filter(candidate => candidate.startsWith(prefix) && !candidate.slice(prefix.length).includes("/"))
|
|
359
|
+
.map(candidate => name(candidate)).sort();
|
|
360
|
+
const maximum = args[1] ?? maxListItems;
|
|
361
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || maximum > maxListItems) throw new RangeError("list maxItems is outside its supported bounds");
|
|
362
|
+
if (names.length > maximum) throw new RangeError("list result exceeds maxItems");
|
|
363
|
+
let units = 0;
|
|
364
|
+
for (const item of names) {
|
|
365
|
+
units += item.length;
|
|
366
|
+
if (units > maxListTextUnits) throw new RangeError("list result cannot exceed 2 MiB of text");
|
|
367
|
+
}
|
|
368
|
+
return names;
|
|
369
|
+
}
|
|
370
|
+
if (operation === "info") {
|
|
371
|
+
const path = authorized(args[0]);
|
|
372
|
+
const node = nodes.get(path);
|
|
373
|
+
return node ? { name: name(path), kind: node.kind, size: new TextEncoder().encode(node.body).byteLength, modifiedAt: node.modifiedAt } : null;
|
|
374
|
+
}
|
|
375
|
+
if (operation === "copyFile") {
|
|
376
|
+
const { node } = existing(args[0]);
|
|
377
|
+
if (node.kind !== "file") throw new Error("copyFile requires a file source");
|
|
378
|
+
const target = authorized(args[1]);
|
|
379
|
+
if (nodes.has(target) && args[2] !== true) throw new Error("copyFile target already exists");
|
|
380
|
+
file(target, node.body); return null;
|
|
381
|
+
}
|
|
382
|
+
if (operation === "move") {
|
|
383
|
+
const source = existing(args[0]);
|
|
384
|
+
const target = authorized(args[1]);
|
|
385
|
+
if (nodes.has(target) && args[2] !== true) throw new Error("move target already exists");
|
|
386
|
+
if (!nodes.has(parent(target)) || nodes.get(parent(target)).kind !== "directory") throw new Error("Desktop test move parent directory does not exist");
|
|
387
|
+
if (source.path === projectRoot || source.path === appDataRoot) throw new Error("move refuses a granted Desktop file root");
|
|
388
|
+
if (source.node.kind === "directory" && contained(source.path, target)) throw new Error("move target cannot be inside its source");
|
|
389
|
+
if (nodes.get(target)?.kind === "directory") throw new Error("move cannot replace a directory");
|
|
390
|
+
if (nodes.has(target)) { nodes.delete(target); notifyWatchers(target); }
|
|
391
|
+
const moving = [...nodes.entries()].filter(([path]) => path === source.path || path.startsWith(source.path + "/"));
|
|
392
|
+
for (const [path] of moving) nodes.delete(path);
|
|
393
|
+
for (const [path, node] of moving) nodes.set(target + path.slice(source.path.length), node);
|
|
394
|
+
notifyWatchers(source.path);
|
|
395
|
+
notifyWatchers(target);
|
|
396
|
+
return null;
|
|
397
|
+
}
|
|
398
|
+
if (operation === "removeFile") {
|
|
399
|
+
const { path, node } = existing(args[0]);
|
|
400
|
+
if (node.kind !== "file") throw new Error("removeFile requires a file path");
|
|
401
|
+
nodes.delete(path); notifyWatchers(path); return null;
|
|
402
|
+
}
|
|
403
|
+
throw new Error("Unsupported Desktop test filesystem operation '" + operation + "'");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
async function processCapability(operation, args) {
|
|
407
|
+
if (operation === "start") {
|
|
408
|
+
const [command, commandArgs = [], options = {}] = args;
|
|
409
|
+
if (typeof command !== "string" || !processGrants.has(command)) throw new Error("Desktop test process is not granted");
|
|
410
|
+
if (!Array.isArray(commandArgs) || commandArgs.length > 1000 || commandArgs.some(value => typeof value !== "string")) {
|
|
411
|
+
throw new TypeError("Desktop test process args must be a bounded string list");
|
|
412
|
+
}
|
|
413
|
+
const stdout = "[desktop-test] " + command + (commandArgs.length ? " " + commandArgs.join(" ") : "") + "\n";
|
|
414
|
+
const maximum = options.maxOutputBytes ?? 4 * 1024 * 1024;
|
|
415
|
+
if (!Number.isSafeInteger(maximum) || maximum < 1 || new TextEncoder().encode(stdout).byteLength > maximum) {
|
|
416
|
+
throw new RangeError("Desktop test process output exceeded maxOutputBytes");
|
|
417
|
+
}
|
|
418
|
+
const handle = nextProcessHandle++;
|
|
419
|
+
const result = Object.freeze({code: 0, signal: null, stdout, stderr: ""});
|
|
420
|
+
processHandles.set(handle, {
|
|
421
|
+
result,
|
|
422
|
+
output: stdout.length === 0 ? [] : [Object.freeze({channel: "stdout", text: stdout})],
|
|
423
|
+
});
|
|
424
|
+
return {handle, pid: 0};
|
|
425
|
+
}
|
|
426
|
+
const handle = args[0];
|
|
427
|
+
if (!Number.isSafeInteger(handle) || handle < 1) throw new TypeError("Desktop test process handle is invalid");
|
|
428
|
+
const process = processHandles.get(handle);
|
|
429
|
+
if (operation === "read") {
|
|
430
|
+
if (!process) throw new Error("Desktop test process handle is unknown or already released");
|
|
431
|
+
return process.output.length === 0 ? null : process.output.shift();
|
|
432
|
+
}
|
|
433
|
+
if (operation === "wait") {
|
|
434
|
+
if (!process) throw new Error("Desktop test process handle is unknown or already released");
|
|
435
|
+
processHandles.delete(handle);
|
|
436
|
+
return {result: process.result, error: null, retained: false};
|
|
437
|
+
}
|
|
438
|
+
if (operation === "stop") {
|
|
439
|
+
processHandles.delete(handle);
|
|
440
|
+
return {result: process?.result ?? null, error: null};
|
|
441
|
+
}
|
|
442
|
+
throw new Error("Unsupported Desktop test process operation '" + operation + "'");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const bridge = Object.freeze({
|
|
446
|
+
platform: ${JSON.stringify(platform)},
|
|
447
|
+
packaged: false,
|
|
448
|
+
projectDirectory: projectRoot,
|
|
449
|
+
projectDirectoryValue() { return projectRoot; },
|
|
450
|
+
environment,
|
|
451
|
+
async invoke(capability, operation, args) {
|
|
452
|
+
if (!Array.isArray(args)) throw new TypeError("Desktop test bridge args must be a list");
|
|
453
|
+
if (capability === "desktop") {
|
|
454
|
+
if (operation === "homeDirectory") return "/velar-test/home";
|
|
455
|
+
if (operation === "appDataDirectory") return appDataRoot;
|
|
456
|
+
if (operation === "projectDirectory") return projectRoot;
|
|
457
|
+
if (operation === "selectedProjectDirectory") return selectedProjectRoot;
|
|
458
|
+
if (operation === "selectProjectDirectory") {
|
|
459
|
+
if (!grants.has("project")) throw new Error("Desktop test application has no project file grant");
|
|
460
|
+
selectedProjectRoot = projectRoot;
|
|
461
|
+
return selectedProjectRoot;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
if (capability === "fs") return fs(operation, args);
|
|
465
|
+
if (capability === "process") return processCapability(operation, args);
|
|
466
|
+
if (capability === "project-changes") return projectChangeCapability(operation, args);
|
|
467
|
+
if (capability === "project-change-test") return projectChangeTest(operation, args);
|
|
468
|
+
throw new Error("Desktop test capability '" + capability + "' is not configured");
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
Object.defineProperty(globalThis, protocol, { value: bridge, enumerable: false, configurable: false, writable: false });
|
|
472
|
+
})();
|
|
473
|
+
`.trim();
|
|
474
|
+
}
|
|
475
|
+
//# sourceMappingURL=test-runtime.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-runtime.js","sourceRoot":"","sources":["../src/test-runtime.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAA0B;IACrE,IAAI,QAAQ,GAAwB,MAAM,CAAC;IAC3C,IAAI,MAAM,GAAG,KAAK,CAAC;IACnB,OAAO,MAAM,CAAC,MAAM,CAAC;QACnB,UAAU;YACR,MAAM,GAAG,IAAI,CAAC;YACd,OAAO,4BAA4B,CAAC,MAAM,EAAE,QAAQ,CAAC,CAAC;QACxD,CAAC;QACD,MAAM,CAAC,UAAkB,EAAE,SAAiB,EAAE,IAAwB;YACpE,IAAI,UAAU,KAAK,cAAc,IAAI,SAAS,KAAK,aAAa;gBAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YAC5F,IAAI,MAAM;gBAAE,MAAM,IAAI,KAAK,CAAC,mEAAmE,CAAC,CAAC;YACjG,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,OAAO,IAAI,IAAI,CAAC,CAAC,CAAC,KAAK,MAAM,EAAE,CAAC;gBACnE,MAAM,IAAI,SAAS,CAAC,6EAA6E,CAAC,CAAC;YACrG,CAAC;YACD,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;YACnB,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;QACxC,CAAC;KACF,CAAC,CAAC;AACL,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,4BAA4B,CAAC,MAA0B,EAAE,QAAQ,GAAwB,MAAM;IAC7G,MAAM,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;IACvD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,WAAW,CAAC,SAAS,CAAC,CAAC;IAC/D,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;2BAOQ,KAAK;kCACE,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAmZ3B,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2BvC,CAAC,IAAI,EAAE,CAAC;AACT,CAAC"}
|