@githolon/testing 0.3.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/LICENSE.md +45 -0
- package/README.md +159 -0
- package/build.mjs +23 -0
- package/index.d.ts +142 -0
- package/package.json +43 -0
- package/src/index.mjs +449 -0
- package/vendor/engine/engine.mjs +1370 -0
- package/vendor/engine/git-fs.mjs +93 -0
- package/vendor/engine/tree.mjs +73 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// AUTO-SYNCED from cloud/holon-host/src/git-fs.mjs by testing/build.mjs — DO NOT EDIT HERE.
|
|
2
|
+
// An isomorphic-git fs adapter over a @bjorn3/browser_wasi_shim PreopenDirectory tree.
|
|
3
|
+
//
|
|
4
|
+
// This is what lets the DO push/fetch the EXACT same in-memory git repo that the holon's gix
|
|
5
|
+
// writes (loose objects under /work/workspaces/<ws>/nomos.git) — no copy, no second store. The
|
|
6
|
+
// holon commits via gix; isomorphic-git reads those loose objects off this same tree and pushes
|
|
7
|
+
// them to the Artifacts remote (and writes fetched objects back for the holon to read).
|
|
8
|
+
//
|
|
9
|
+
// isomorphic-git uses the Node-style fs.promises surface and is sensitive to error `.code`
|
|
10
|
+
// (ENOENT/EEXIST/ENOTDIR/EISDIR), so we set those exactly.
|
|
11
|
+
|
|
12
|
+
function fsErr(code, p) { const e = new Error(`${code}: ${p}`); e.code = code; return e; }
|
|
13
|
+
|
|
14
|
+
// Build an fs ({promises:{...}}) rooted at `mount` (e.g. "/work") over `preopen.dir`.
|
|
15
|
+
export function makeGitFs(preopen, mount, { File, Directory }) {
|
|
16
|
+
const root = preopen.dir; // Directory: { contents: Map }
|
|
17
|
+
const isDir = (n) => n && n.contents instanceof Map;
|
|
18
|
+
|
|
19
|
+
function parts(p) {
|
|
20
|
+
if (p === mount) return [];
|
|
21
|
+
if (!p.startsWith(mount + "/")) throw fsErr("ENOENT", p);
|
|
22
|
+
const rel = p.slice(mount.length + 1).replace(/\/+$/, "");
|
|
23
|
+
return rel === "" ? [] : rel.split("/");
|
|
24
|
+
}
|
|
25
|
+
function resolve(ps) {
|
|
26
|
+
let cur = root;
|
|
27
|
+
for (const part of ps) {
|
|
28
|
+
if (!isDir(cur)) return null;
|
|
29
|
+
const next = cur.contents.get(part);
|
|
30
|
+
if (next === undefined) return null;
|
|
31
|
+
cur = next;
|
|
32
|
+
}
|
|
33
|
+
return cur;
|
|
34
|
+
}
|
|
35
|
+
const parent = (ps) => resolve(ps.slice(0, -1));
|
|
36
|
+
const leaf = (ps) => ps[ps.length - 1];
|
|
37
|
+
|
|
38
|
+
const promises = {
|
|
39
|
+
async readFile(p, opts) {
|
|
40
|
+
const n = resolve(parts(p));
|
|
41
|
+
if (n === null) throw fsErr("ENOENT", p);
|
|
42
|
+
if (isDir(n)) throw fsErr("EISDIR", p);
|
|
43
|
+
const data = n.data ?? new Uint8Array(0);
|
|
44
|
+
const encoding = typeof opts === "string" ? opts : opts && opts.encoding;
|
|
45
|
+
return encoding ? new TextDecoder().decode(data) : new Uint8Array(data);
|
|
46
|
+
},
|
|
47
|
+
async writeFile(p, data) {
|
|
48
|
+
const ps = parts(p);
|
|
49
|
+
const par = parent(ps);
|
|
50
|
+
if (!isDir(par)) throw fsErr("ENOENT", p);
|
|
51
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : new Uint8Array(data);
|
|
52
|
+
par.contents.set(leaf(ps), new File(bytes));
|
|
53
|
+
},
|
|
54
|
+
async unlink(p) {
|
|
55
|
+
const ps = parts(p), par = parent(ps);
|
|
56
|
+
if (!isDir(par) || !par.contents.delete(leaf(ps))) throw fsErr("ENOENT", p);
|
|
57
|
+
},
|
|
58
|
+
async readdir(p) {
|
|
59
|
+
const n = resolve(parts(p));
|
|
60
|
+
if (n === null) throw fsErr("ENOENT", p);
|
|
61
|
+
if (!isDir(n)) throw fsErr("ENOTDIR", p);
|
|
62
|
+
return [...n.contents.keys()];
|
|
63
|
+
},
|
|
64
|
+
async mkdir(p) {
|
|
65
|
+
const ps = parts(p), par = parent(ps);
|
|
66
|
+
if (!isDir(par)) throw fsErr("ENOENT", p);
|
|
67
|
+
if (par.contents.has(leaf(ps))) throw fsErr("EEXIST", p);
|
|
68
|
+
par.contents.set(leaf(ps), new Directory(new Map()));
|
|
69
|
+
},
|
|
70
|
+
async rmdir(p) {
|
|
71
|
+
const ps = parts(p), par = parent(ps);
|
|
72
|
+
if (!isDir(par)) throw fsErr("ENOENT", p);
|
|
73
|
+
par.contents.delete(leaf(ps));
|
|
74
|
+
},
|
|
75
|
+
async stat(p) { return promises.lstat(p); },
|
|
76
|
+
async lstat(p) {
|
|
77
|
+
const n = resolve(parts(p));
|
|
78
|
+
if (n === null) throw fsErr("ENOENT", p);
|
|
79
|
+
const dir = isDir(n);
|
|
80
|
+
return {
|
|
81
|
+
type: dir ? "dir" : "file",
|
|
82
|
+
mode: dir ? 0o040000 : 0o100644,
|
|
83
|
+
size: dir ? 0 : (n.data ? n.data.length : 0),
|
|
84
|
+
ino: 0, mtimeMs: 0, ctimeMs: 0, uid: 1, gid: 1, dev: 1,
|
|
85
|
+
isFile: () => !dir, isDirectory: () => dir, isSymbolicLink: () => false,
|
|
86
|
+
};
|
|
87
|
+
},
|
|
88
|
+
async readlink(p) { throw fsErr("EINVAL", p); },
|
|
89
|
+
async symlink() { throw fsErr("EPERM", "symlink unsupported"); },
|
|
90
|
+
async chmod() {},
|
|
91
|
+
};
|
|
92
|
+
return { promises };
|
|
93
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
// AUTO-SYNCED from cloud/holon-host/src/tree.mjs by testing/build.mjs — DO NOT EDIT HERE.
|
|
2
|
+
// /work tree ⇄ flat manifest bytes — the decoupled twin of opfs_tree.mjs (serialize/deserialize
|
|
3
|
+
// only, no OPFS I/O). On the edge the manifest bytes round-trip through R2 instead of an OPFS
|
|
4
|
+
// SyncAccessHandle. Byte format is identical to opfs_tree.mjs so a snapshot is portable between
|
|
5
|
+
// the browser holon and the edge holon:
|
|
6
|
+
// [u32 count] then per entry: [u8 isDir][u32 pathLen][path][u32 byteLen][bytes]
|
|
7
|
+
const enc = new TextEncoder();
|
|
8
|
+
const dec = new TextDecoder();
|
|
9
|
+
|
|
10
|
+
function flatten(dir, prefix, out) {
|
|
11
|
+
for (const [name, inode] of dir.contents) {
|
|
12
|
+
const path = prefix ? prefix + "/" + name : name;
|
|
13
|
+
if (inode.contents instanceof Map) {
|
|
14
|
+
out.push({ path, dir: true });
|
|
15
|
+
flatten(inode, path, out);
|
|
16
|
+
} else {
|
|
17
|
+
out.push({ path, dir: false, bytes: inode.data ?? new Uint8Array(0) });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function serializeTree(workTree) {
|
|
23
|
+
const entries = [];
|
|
24
|
+
flatten(workTree, "", entries);
|
|
25
|
+
const chunks = [];
|
|
26
|
+
const header = new Uint8Array(4);
|
|
27
|
+
new DataView(header.buffer).setUint32(0, entries.length, true);
|
|
28
|
+
chunks.push(header);
|
|
29
|
+
for (const e of entries) {
|
|
30
|
+
const pathBytes = enc.encode(e.path);
|
|
31
|
+
const body = e.dir ? new Uint8Array(0) : (e.bytes ?? new Uint8Array(0));
|
|
32
|
+
const meta = new Uint8Array(9);
|
|
33
|
+
const dv = new DataView(meta.buffer);
|
|
34
|
+
dv.setUint8(0, e.dir ? 1 : 0);
|
|
35
|
+
dv.setUint32(1, pathBytes.length, true);
|
|
36
|
+
dv.setUint32(5, body.length, true);
|
|
37
|
+
chunks.push(meta, pathBytes, body);
|
|
38
|
+
}
|
|
39
|
+
let total = 0;
|
|
40
|
+
for (const c of chunks) total += c.length;
|
|
41
|
+
const out = new Uint8Array(total);
|
|
42
|
+
let off = 0;
|
|
43
|
+
for (const c of chunks) { out.set(c, off); off += c.length; }
|
|
44
|
+
return out;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function deserializeTree(bytes, { File, Directory }) {
|
|
48
|
+
const dv = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
49
|
+
let off = 0;
|
|
50
|
+
const count = dv.getUint32(off, true); off += 4;
|
|
51
|
+
const root = new Directory(new Map());
|
|
52
|
+
for (let i = 0; i < count; i++) {
|
|
53
|
+
const isDir = dv.getUint8(off); off += 1;
|
|
54
|
+
const pathLen = dv.getUint32(off, true); off += 4;
|
|
55
|
+
const byteLen = dv.getUint32(off, true); off += 4;
|
|
56
|
+
const path = dec.decode(bytes.subarray(off, off + pathLen)); off += pathLen;
|
|
57
|
+
const body = bytes.subarray(off, off + byteLen); off += byteLen;
|
|
58
|
+
const parts = path.split("/");
|
|
59
|
+
let cur = root;
|
|
60
|
+
for (let p = 0; p < parts.length - 1; p++) {
|
|
61
|
+
let next = cur.contents.get(parts[p]);
|
|
62
|
+
if (!next) { next = new Directory(new Map()); cur.contents.set(parts[p], next); }
|
|
63
|
+
cur = next;
|
|
64
|
+
}
|
|
65
|
+
const leaf = parts[parts.length - 1];
|
|
66
|
+
if (isDir) {
|
|
67
|
+
if (!cur.contents.get(leaf)) cur.contents.set(leaf, new Directory(new Map()));
|
|
68
|
+
} else {
|
|
69
|
+
cur.contents.set(leaf, new File(body.slice()));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return root.contents;
|
|
73
|
+
}
|