@openpalm/lib 0.11.0-beta.9 → 0.11.0-rc.18
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 +1 -1
- package/package.json +1 -1
- package/src/control-plane/akm-sources.test.ts +206 -0
- package/src/control-plane/akm-sources.ts +234 -0
- package/src/control-plane/akm-user-env.test.ts +142 -0
- package/src/control-plane/akm-user-env.ts +167 -0
- package/src/control-plane/backup.ts +14 -5
- package/src/control-plane/channels.ts +48 -29
- package/src/control-plane/cleanup-guardrails.test.ts +1 -1
- package/src/control-plane/compose-args.test.ts +69 -30
- package/src/control-plane/compose-args.ts +62 -8
- package/src/control-plane/config-persistence.ts +102 -136
- package/src/control-plane/core-assets.ts +45 -60
- package/src/control-plane/defaults.ts +16 -0
- package/src/control-plane/docker.ts +15 -14
- package/src/control-plane/env.test.ts +10 -10
- package/src/control-plane/env.ts +16 -1
- package/src/control-plane/extends-support.test.ts +8 -8
- package/src/control-plane/fs-atomic.ts +15 -0
- package/src/control-plane/home.ts +34 -46
- package/src/control-plane/host-akm-sharing.test.ts +145 -0
- package/src/control-plane/host-akm-sharing.ts +129 -0
- package/src/control-plane/host-opencode.test.ts +82 -10
- package/src/control-plane/host-opencode.ts +42 -13
- package/src/control-plane/install-edge-cases.test.ts +100 -136
- package/src/control-plane/install-lock.ts +7 -7
- package/src/control-plane/lifecycle.ts +45 -40
- package/src/control-plane/markdown-task.ts +30 -50
- package/src/control-plane/migrations.test.ts +272 -0
- package/src/control-plane/migrations.ts +423 -0
- package/src/control-plane/opencode-client.ts +1 -1
- package/src/control-plane/paths.ts +61 -46
- package/src/control-plane/profile-ids.ts +21 -0
- package/src/control-plane/provider-models.ts +3 -3
- package/src/control-plane/registry.test.ts +107 -90
- package/src/control-plane/registry.ts +301 -110
- package/src/control-plane/rollback.ts +8 -38
- package/src/control-plane/scheduler.ts +10 -7
- package/src/control-plane/secret-audit.test.ts +159 -0
- package/src/control-plane/secret-audit.ts +255 -0
- package/src/control-plane/secret-mappings.ts +2 -2
- package/src/control-plane/secrets-files.test.ts +99 -0
- package/src/control-plane/secrets-files.ts +113 -0
- package/src/control-plane/secrets.ts +113 -86
- package/src/control-plane/setup-config.schema.json +1 -1
- package/src/control-plane/setup-status.ts +6 -11
- package/src/control-plane/setup.test.ts +137 -61
- package/src/control-plane/setup.ts +82 -63
- package/src/control-plane/skeleton-guardrail.test.ts +66 -56
- package/src/control-plane/spec-to-env.test.ts +63 -26
- package/src/control-plane/spec-to-env.ts +51 -14
- package/src/control-plane/task-files.test.ts +45 -0
- package/src/control-plane/task-files.ts +51 -0
- package/src/control-plane/types.ts +2 -4
- package/src/control-plane/ui-assets.test.ts +333 -0
- package/src/control-plane/ui-assets.ts +290 -142
- package/src/control-plane/validate.ts +13 -15
- package/src/index.ts +96 -26
- package/src/control-plane/akm-vault.test.ts +0 -105
- package/src/control-plane/akm-vault.ts +0 -311
- package/src/control-plane/core-assets.test.ts +0 -104
- package/src/control-plane/migrate-0110.test.ts +0 -177
- package/src/control-plane/migrate-0110.ts +0 -99
- package/src/control-plane/registry-components.test.ts +0 -391
- package/src/control-plane/stack-spec.test.ts +0 -94
- package/src/control-plane/stack-spec.ts +0 -67
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach, spyOn, mock } from "bun:test";
|
|
2
|
+
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import {
|
|
8
|
+
resolveUiBuildDir, readUiBuildVersion, UI_VERSION_STAMP,
|
|
9
|
+
seedOpenPalmDir, SKELETON_VERSION_STAMP,
|
|
10
|
+
uiUpdateChannel, checkAndUpdateUiBuild,
|
|
11
|
+
} from "./ui-assets.js";
|
|
12
|
+
|
|
13
|
+
let root = "";
|
|
14
|
+
let opHome = "";
|
|
15
|
+
let repoRoot = "";
|
|
16
|
+
let dataUi = "";
|
|
17
|
+
let bundledUi = "";
|
|
18
|
+
const saved: Record<string, string | undefined> = {};
|
|
19
|
+
|
|
20
|
+
function makeBuild(dir: string, version: string | null): void {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
writeFileSync(join(dir, "index.js"), "// ui server\n");
|
|
23
|
+
if (version !== null) writeFileSync(join(dir, UI_VERSION_STAMP), `${version}\n`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
root = mkdtempSync(join(tmpdir(), "ui-assets-"));
|
|
28
|
+
opHome = join(root, "ophome");
|
|
29
|
+
repoRoot = join(root, "repo");
|
|
30
|
+
dataUi = join(opHome, "data", "ui");
|
|
31
|
+
bundledUi = join(repoRoot, "packages", "ui", "build"); // resolveLocalUiBuild() candidate 1
|
|
32
|
+
saved.OP_HOME = process.env.OP_HOME;
|
|
33
|
+
saved.OPENPALM_REPO_ROOT = process.env.OPENPALM_REPO_ROOT;
|
|
34
|
+
process.env.OP_HOME = opHome;
|
|
35
|
+
// Pin the bundled candidate to a controlled location so the resolver never
|
|
36
|
+
// discovers the real packages/ui/build via its source-relative fallback.
|
|
37
|
+
// Default: an EMPTY build dir (exists but no index.js) = "no bundled build".
|
|
38
|
+
process.env.OPENPALM_REPO_ROOT = repoRoot;
|
|
39
|
+
mkdirSync(bundledUi, { recursive: true });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
rmSync(root, { recursive: true, force: true });
|
|
44
|
+
for (const k of ["OP_HOME", "OPENPALM_REPO_ROOT"] as const) {
|
|
45
|
+
if (saved[k] === undefined) delete process.env[k];
|
|
46
|
+
else process.env[k] = saved[k];
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe("readUiBuildVersion", () => {
|
|
51
|
+
it("reads the stamp, or null when absent", () => {
|
|
52
|
+
makeBuild(dataUi, "0.11.0");
|
|
53
|
+
expect(readUiBuildVersion(dataUi)).toBe("0.11.0");
|
|
54
|
+
makeBuild(bundledUi, null);
|
|
55
|
+
expect(readUiBuildVersion(bundledUi)).toBeNull();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("resolveUiBuildDir — version-aware selection", () => {
|
|
60
|
+
it("uses data/ui when only it exists", () => {
|
|
61
|
+
makeBuild(dataUi, "0.11.0");
|
|
62
|
+
expect(resolveUiBuildDir()).toBe(dataUi);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("uses bundled when only it exists", () => {
|
|
66
|
+
makeBuild(bundledUi, "0.11.0");
|
|
67
|
+
process.env.OPENPALM_REPO_ROOT = repoRoot;
|
|
68
|
+
expect(resolveUiBuildDir()).toBe(bundledUi);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("prefers data/ui only when it is strictly NEWER than bundled", () => {
|
|
72
|
+
makeBuild(dataUi, "0.12.0");
|
|
73
|
+
makeBuild(bundledUi, "0.11.0");
|
|
74
|
+
process.env.OPENPALM_REPO_ROOT = repoRoot;
|
|
75
|
+
expect(resolveUiBuildDir()).toBe(dataUi);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("prefers bundled when it is newer than data/ui (fixes stale-data/ui shadowing)", () => {
|
|
79
|
+
makeBuild(dataUi, "0.11.0");
|
|
80
|
+
makeBuild(bundledUi, "0.12.0");
|
|
81
|
+
process.env.OPENPALM_REPO_ROOT = repoRoot;
|
|
82
|
+
expect(resolveUiBuildDir()).toBe(bundledUi);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("prefers bundled when versions are equal", () => {
|
|
86
|
+
makeBuild(dataUi, "0.11.0");
|
|
87
|
+
makeBuild(bundledUi, "0.11.0");
|
|
88
|
+
process.env.OPENPALM_REPO_ROOT = repoRoot;
|
|
89
|
+
expect(resolveUiBuildDir()).toBe(bundledUi);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("prefers bundled when data/ui is unstamped (cannot prove it is newer)", () => {
|
|
93
|
+
makeBuild(dataUi, null);
|
|
94
|
+
makeBuild(bundledUi, "0.11.0");
|
|
95
|
+
process.env.OPENPALM_REPO_ROOT = repoRoot;
|
|
96
|
+
expect(resolveUiBuildDir()).toBe(bundledUi);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("falls back to the data/ui path when nothing is present (caller seeds)", () => {
|
|
100
|
+
expect(resolveUiBuildDir()).toBe(dataUi);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe("seedOpenPalmDir — version guard (P2)", () => {
|
|
105
|
+
const seededFile = () => join(opHome, "config", "stack", "x.txt");
|
|
106
|
+
const stamp = () => join(opHome, SKELETON_VERSION_STAMP);
|
|
107
|
+
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
// Local skeleton source at OPENPALM_REPO_ROOT/.openpalm (candidate 1).
|
|
110
|
+
mkdirSync(join(repoRoot, ".openpalm", "config", "stack"), { recursive: true });
|
|
111
|
+
writeFileSync(join(repoRoot, ".openpalm", "config", "stack", "x.txt"), "seed\n");
|
|
112
|
+
mkdirSync(opHome, { recursive: true });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("seeds once and stamps the version", async () => {
|
|
116
|
+
await seedOpenPalmDir("v1", opHome, join(opHome, "config"), join(opHome, "data"));
|
|
117
|
+
expect(existsSync(seededFile())).toBe(true);
|
|
118
|
+
expect(readFileSync(stamp(), "utf-8").trim()).toBe("v1");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does NOT re-seed (or re-materialize a removed file) for the same version", async () => {
|
|
122
|
+
await seedOpenPalmDir("v1", opHome, join(opHome, "config"), join(opHome, "data"));
|
|
123
|
+
rmSync(seededFile(), { force: true });
|
|
124
|
+
await seedOpenPalmDir("v1", opHome, join(opHome, "config"), join(opHome, "data"));
|
|
125
|
+
expect(existsSync(seededFile())).toBe(false); // guard skipped the copy
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("re-seeds on a version change", async () => {
|
|
129
|
+
await seedOpenPalmDir("v1", opHome, join(opHome, "config"), join(opHome, "data"));
|
|
130
|
+
rmSync(seededFile(), { force: true });
|
|
131
|
+
await seedOpenPalmDir("v2", opHome, join(opHome, "config"), join(opHome, "data"));
|
|
132
|
+
expect(existsSync(seededFile())).toBe(true);
|
|
133
|
+
expect(readFileSync(stamp(), "utf-8").trim()).toBe("v2");
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ── uiUpdateChannel ───────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe("uiUpdateChannel", () => {
|
|
140
|
+
it("returns 'latest' for a stable version", () => {
|
|
141
|
+
expect(uiUpdateChannel("0.11.0")).toBe("latest");
|
|
142
|
+
expect(uiUpdateChannel("1.0.0")).toBe("latest");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("returns 'next' for a prerelease version (contains '-')", () => {
|
|
146
|
+
expect(uiUpdateChannel("0.11.0-rc.2")).toBe("next");
|
|
147
|
+
expect(uiUpdateChannel("0.11.0-beta.5")).toBe("next");
|
|
148
|
+
expect(uiUpdateChannel("1.0.0-alpha.1")).toBe("next");
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── npm integrity verification (via checkAndUpdateUiBuild) ────────────────────
|
|
153
|
+
//
|
|
154
|
+
// We mock globalThis.fetch to avoid real network calls. The integrity paths
|
|
155
|
+
// are exercised through checkAndUpdateUiBuild → downloadNpmUiBundle (for the
|
|
156
|
+
// missing-integrity and mismatch cases) and through checkAndUpdateUiBuild
|
|
157
|
+
// returning {updated:false} early (for the up-to-date case).
|
|
158
|
+
|
|
159
|
+
/** Build a correct sha512 SRI string for the given bytes. */
|
|
160
|
+
function makeSri(data: Uint8Array): string {
|
|
161
|
+
const digest = createHash("sha512").update(data).digest("base64");
|
|
162
|
+
return `sha512-${digest}`;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
describe("npm integrity verification (fail-closed)", () => {
|
|
166
|
+
let savedFetch: typeof globalThis.fetch;
|
|
167
|
+
|
|
168
|
+
beforeEach(() => {
|
|
169
|
+
savedFetch = globalThis.fetch;
|
|
170
|
+
// Set forceRemote context: no local build available for these tests.
|
|
171
|
+
// (data/ui has no index.js, bundledUi dir exists but is empty → no local build)
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
afterEach(() => {
|
|
175
|
+
globalThis.fetch = savedFetch;
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("throws when the manifest has no integrity hash (fail-closed)", async () => {
|
|
179
|
+
// manifest fetch returns a version with no integrity
|
|
180
|
+
globalThis.fetch = async (_url: string | URL | Request) => {
|
|
181
|
+
const url = String(typeof _url === "string" ? _url : (_url as Request).url ?? _url);
|
|
182
|
+
if (url.includes("registry.npmjs.org")) {
|
|
183
|
+
return new Response(
|
|
184
|
+
JSON.stringify({
|
|
185
|
+
version: "0.11.0",
|
|
186
|
+
dist: { tarball: "https://registry.npmjs.org/tarball.tgz" },
|
|
187
|
+
// integrity intentionally omitted
|
|
188
|
+
}),
|
|
189
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
// tarball fetch — should NOT be reached because we throw before it
|
|
193
|
+
return new Response("not-reached", { status: 200 });
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const result = await checkAndUpdateUiBuild("0.11.0-beta.1", join(dataUi, ".."));
|
|
197
|
+
// Missing integrity → non-fatal error path
|
|
198
|
+
expect(result.updated).toBe(false);
|
|
199
|
+
expect(result.error).toMatch(/no integrity hash/i);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("throws when the tarball bytes do not match the stated integrity hash", async () => {
|
|
203
|
+
const fakeData = new Uint8Array([1, 2, 3, 4]);
|
|
204
|
+
const wrongSri = `sha512-${Buffer.from("wrong").toString("base64")}`;
|
|
205
|
+
|
|
206
|
+
globalThis.fetch = async (_url: string | URL | Request) => {
|
|
207
|
+
const url = String(typeof _url === "string" ? _url : (_url as Request).url ?? _url);
|
|
208
|
+
if (url.includes("registry.npmjs.org") && !url.includes("tarball")) {
|
|
209
|
+
return new Response(
|
|
210
|
+
JSON.stringify({
|
|
211
|
+
version: "0.99.0", // newer than anything on disk
|
|
212
|
+
dist: { tarball: "https://registry.npmjs.org/tarball.tgz", integrity: wrongSri },
|
|
213
|
+
}),
|
|
214
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
// tarball response — bytes deliberately do NOT match wrongSri
|
|
218
|
+
return new Response(fakeData, { status: 200 });
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = await checkAndUpdateUiBuild("0.11.0", join(dataUi, ".."));
|
|
222
|
+
expect(result.updated).toBe(false);
|
|
223
|
+
expect(result.error).toMatch(/integrity mismatch/i);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// ── checkAndUpdateUiBuild ─────────────────────────────────────────────────────
|
|
228
|
+
|
|
229
|
+
describe("checkAndUpdateUiBuild", () => {
|
|
230
|
+
let savedFetch: typeof globalThis.fetch;
|
|
231
|
+
|
|
232
|
+
beforeEach(() => {
|
|
233
|
+
savedFetch = globalThis.fetch;
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
afterEach(() => {
|
|
237
|
+
globalThis.fetch = savedFetch;
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
/** Make a minimal npm manifest response for a given version. */
|
|
241
|
+
function manifestResponse(version: string, integrity?: string) {
|
|
242
|
+
return new Response(
|
|
243
|
+
JSON.stringify({
|
|
244
|
+
version,
|
|
245
|
+
dist: {
|
|
246
|
+
tarball: "https://registry.npmjs.org/tarball.tgz",
|
|
247
|
+
...(integrity !== undefined ? { integrity } : {}),
|
|
248
|
+
},
|
|
249
|
+
}),
|
|
250
|
+
{ status: 200, headers: { "Content-Type": "application/json" } }
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
it("returns {updated:false} when the npm channel version is not newer than on-disk stamp", async () => {
|
|
255
|
+
// Seed data/ui with a stamped build
|
|
256
|
+
makeBuild(dataUi, "0.11.0");
|
|
257
|
+
|
|
258
|
+
globalThis.fetch = async () => manifestResponse("0.11.0"); // same version
|
|
259
|
+
const result = await checkAndUpdateUiBuild("0.11.0", join(opHome, "data"));
|
|
260
|
+
expect(result.updated).toBe(false);
|
|
261
|
+
expect(result.latestVersion).toBe("0.11.0");
|
|
262
|
+
expect(result.error).toBeUndefined();
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("returns {updated:false} when the npm channel version is older than on-disk stamp", async () => {
|
|
266
|
+
makeBuild(dataUi, "0.12.0");
|
|
267
|
+
|
|
268
|
+
globalThis.fetch = async () => manifestResponse("0.11.0"); // older
|
|
269
|
+
const result = await checkAndUpdateUiBuild("0.12.0", join(opHome, "data"));
|
|
270
|
+
expect(result.updated).toBe(false);
|
|
271
|
+
expect(result.latestVersion).toBe("0.11.0");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("returns {updated:false, error} when the manifest fetch rejects (non-fatal)", async () => {
|
|
275
|
+
globalThis.fetch = async () => { throw new Error("network failure"); };
|
|
276
|
+
|
|
277
|
+
const result = await checkAndUpdateUiBuild("0.11.0", join(opHome, "data"));
|
|
278
|
+
expect(result.updated).toBe(false);
|
|
279
|
+
expect(result.latestVersion).toBeNull();
|
|
280
|
+
expect(result.error).toMatch(/network failure/i);
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
it("returns {updated:false, error} when the registry returns a non-OK status", async () => {
|
|
284
|
+
globalThis.fetch = async () => new Response("not found", { status: 404 });
|
|
285
|
+
|
|
286
|
+
const result = await checkAndUpdateUiBuild("0.11.0", join(opHome, "data"));
|
|
287
|
+
expect(result.updated).toBe(false);
|
|
288
|
+
expect(result.error).toBeDefined();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it("attempts an update when the on-disk build is unstamped (legacy data/ui)", async () => {
|
|
292
|
+
// Unstamped data/ui — cannot compare, so it should try to refresh from npm.
|
|
293
|
+
// We give it a manifest with missing integrity so it fails non-fatally
|
|
294
|
+
// (avoids needing a real tarball), but we confirm it DID attempt the download.
|
|
295
|
+
makeBuild(dataUi, null); // unstamped
|
|
296
|
+
|
|
297
|
+
let manifestFetched = false;
|
|
298
|
+
globalThis.fetch = async (_url: string | URL | Request) => {
|
|
299
|
+
const url = String(typeof _url === "string" ? _url : (_url as Request).url ?? _url);
|
|
300
|
+
if (url.includes("registry.npmjs.org")) {
|
|
301
|
+
manifestFetched = true;
|
|
302
|
+
// Return a manifest without integrity so downloadNpmUiBundle throws
|
|
303
|
+
return manifestResponse("0.11.1");
|
|
304
|
+
}
|
|
305
|
+
return new Response("", { status: 200 });
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const result = await checkAndUpdateUiBuild("0.11.0", join(opHome, "data"));
|
|
309
|
+
expect(manifestFetched).toBe(true);
|
|
310
|
+
// non-fatal: missing integrity → error path
|
|
311
|
+
expect(result.updated).toBe(false);
|
|
312
|
+
expect(result.error).toBeDefined();
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it("attempts an update when npm has a newer version than the on-disk stamp", async () => {
|
|
316
|
+
makeBuild(dataUi, "0.11.0");
|
|
317
|
+
|
|
318
|
+
let manifestFetched = false;
|
|
319
|
+
globalThis.fetch = async (_url: string | URL | Request) => {
|
|
320
|
+
const url = String(typeof _url === "string" ? _url : (_url as Request).url ?? _url);
|
|
321
|
+
if (url.includes("registry.npmjs.org")) {
|
|
322
|
+
manifestFetched = true;
|
|
323
|
+
return manifestResponse("0.12.0"); // newer — no integrity → non-fatal error
|
|
324
|
+
}
|
|
325
|
+
return new Response("", { status: 200 });
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
const result = await checkAndUpdateUiBuild("0.11.0", join(opHome, "data"));
|
|
329
|
+
expect(manifestFetched).toBe(true);
|
|
330
|
+
expect(result.updated).toBe(false);
|
|
331
|
+
expect(result.error).toMatch(/no integrity hash/i);
|
|
332
|
+
});
|
|
333
|
+
});
|