omakit 0.5.0 → 0.6.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/README.md +39 -32
- package/blocks/history.json +68 -0
- package/blocks/run/NOTICE +12 -0
- package/blocks/run/Run.qml +242 -0
- package/blocks/run/run-supervisor.py +522 -0
- package/blocks/store/NOTICE +12 -0
- package/blocks/store/Store.qml +157 -0
- package/blocks/store/store-helper.py +431 -0
- package/package.json +12 -5
- package/skills/omarchy-plugin-audit/SKILL.md +11 -5
- package/skills/omarchy-plugin-build/SKILL.md +164 -0
- package/skills/omarchy-plugin-check/SKILL.md +7 -4
- package/skills/omarchy-plugin-submit/SKILL.md +4 -1
- package/skills/omarchy-plugin-validation-watch/SKILL.md +5 -2
- package/skills/omarchy-plugin-weigh/SKILL.md +13 -2
- package/tests/fixtures/weigh/clean/Widget.qml +19 -0
- package/tests/fixtures/weigh/clean/manifest.json +9 -0
- package/tests/fixtures/weigh/clean/tests/harness.qml +7 -0
- package/tests/fixtures/weigh/idle-panel/Panel.qml +65 -0
- package/tests/fixtures/weigh/idle-panel/manifest.json +9 -0
- package/tests/fixtures/weigh/poller/Service.qml +50 -0
- package/tests/fixtures/weigh/poller/manifest.json +9 -0
- package/tests/fixtures/weigh/timer-180ms/Widget.qml +25 -0
- package/tests/fixtures/weigh/timer-180ms/manifest.json +9 -0
- package/tests/lab/run/harness/scenarios/controls.sh +6 -0
- package/tests/lab/run/harness/scenarios/envprobe.sh +10 -0
- package/tests/lab/run/harness/scenarios/forge.sh +11 -0
- package/tests/lab/run/harness/scenarios/holder.sh +5 -0
- package/tests/lab/run/harness/scenarios/orphan.sh +6 -0
- package/tests/lab/run/harness/scenarios/stall.sh +5 -0
- package/tests/lab/run/harness/scenarios/stubborn.sh +5 -0
- package/tests/lab/run/harness/scenarios/tree.sh +7 -0
- package/tests/lab/run/harness/shell.qml +84 -0
- package/tests/lab/run/report.py +217 -0
- package/tests/lab/run/suite.sh +106 -0
- package/tests/lab/store/harness/shell.qml +73 -0
- package/tests/lab/store/report.py +133 -0
- package/tests/lab/store/suite.sh +109 -0
- package/tests/parity/corpus.mjs +8 -3
- package/tests/parity/run.mjs +4 -4
- package/tools/audit/audit.mjs +17 -6
- package/tools/audit/git.mjs +3 -3
- package/tools/audit/report.mjs +31 -5
- package/tools/blocks/add.mjs +138 -0
- package/tools/blocks/commit.json +5 -0
- package/tools/blocks/record-commit.mjs +77 -0
- package/tools/blocks/registry.mjs +191 -0
- package/tools/blocks/stamp.mjs +61 -0
- package/tools/inspect/contract.mjs +37 -6
- package/tools/inspect/functions.mjs +236 -12
- package/tools/inspect/helpers.mjs +217 -0
- package/tools/inspect/inspect.mjs +71 -5
- package/tools/inspect/measure-functions.mjs +12 -3
- package/tools/inspect/patterns.mjs +35 -18
- package/tools/inspect/processes.mjs +38 -5
- package/tools/inspect/report.mjs +33 -6
- package/tools/inspect/writes.mjs +22 -4
- package/tools/lab/guest.mjs +155 -0
- package/tools/lab/harness.sh +119 -0
- package/tools/lab/host.mjs +177 -0
- package/tools/lab/inspect.mjs +240 -0
- package/tools/lab/omarchy.gpg +13 -0
- package/tools/lab/patches/omarchy-iso-test.patch +351 -0
- package/tools/lab/paths.mjs +173 -0
- package/tools/lab/pin.json +42 -0
- package/tools/lab/pin.mjs +64 -0
- package/tools/lab/prune.mjs +68 -0
- package/tools/lab/qemu.mjs +153 -0
- package/tools/lab/qmp-cli.mjs +21 -0
- package/tools/lab/report.mjs +183 -0
- package/tools/lab/run.mjs +344 -0
- package/tools/lab/setup.mjs +430 -0
- package/tools/lab/suites/run.sh +35 -0
- package/tools/lab/suites/store.sh +41 -0
- package/tools/lab/suites/weigh.sh +196 -0
- package/tools/lab/suites.mjs +142 -0
- package/tools/lab/verify.mjs +134 -0
- package/tools/marketplace/README.md +38 -1
- package/tools/marketplace/banner.mjs +23 -2
- package/tools/marketplace/cli.mjs +449 -146
- package/tools/marketplace/completion-check.mjs +27 -1
- package/tools/marketplace/completion.mjs +32 -4
- package/tools/marketplace/doctor.mjs +47 -9
- package/tools/marketplace/github.mjs +52 -6
- package/tools/marketplace/local-transport.mjs +1 -1
- package/tools/marketplace/options.mjs +16 -5
- package/tools/marketplace/outcome.mjs +244 -0
- package/tools/marketplace/pin.mjs +178 -33
- package/tools/marketplace/setup.mjs +16 -15
- package/tools/marketplace/tree.mjs +1 -1
- package/tools/marketplace/upgrade.mjs +5 -5
- package/tools/marketplace/usage.mjs +116 -72
- package/tools/subject/resolve.mjs +19 -6
- package/tools/weigh/audit.mjs +47 -16
- package/tools/weigh/config.mjs +105 -24
- package/tools/weigh/list.mjs +10 -1
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
// `omakit lab inspect`, and the lab lines of `omakit doctor`: what the pin
|
|
2
|
+
// advertises, what is on disk, whether it was verified, what the host is
|
|
3
|
+
// missing, and what each missing thing costs. Read-only: no directory is
|
|
4
|
+
// created, no file is written, nothing is fetched. `--verify` re-hashes
|
|
5
|
+
// the ISO and re-checks the signature now instead of reporting the record.
|
|
6
|
+
|
|
7
|
+
import { existsSync, readdirSync, statSync } from "node:fs"
|
|
8
|
+
import { join } from "node:path"
|
|
9
|
+
import { LAB_DIR, bytesBoth, durationWords, labPin } from "./pin.mjs"
|
|
10
|
+
import { allocatedBytes, labLayout, readJson } from "./paths.mjs"
|
|
11
|
+
import { BUILD_COMMANDS, VERIFY_COMMANDS, freeBytesAt, probeCommands, probeRunHost } from "./host.mjs"
|
|
12
|
+
import { judgeRelease, sha256File } from "./verify.mjs"
|
|
13
|
+
import { qmpAlive } from "./qemu.mjs"
|
|
14
|
+
|
|
15
|
+
/** The names the base directory holds; a manifest names the same. */
|
|
16
|
+
export const BASE_FILES = Object.freeze({
|
|
17
|
+
disk: "base.qcow2",
|
|
18
|
+
vars: "firmware-vars.template",
|
|
19
|
+
key: "id_ed25519",
|
|
20
|
+
publicKey: "id_ed25519.pub",
|
|
21
|
+
manifest: "manifest.json",
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
/** The verified ISO's place under downloads: by digest, so a second release never shares a directory with the first. */
|
|
25
|
+
export function downloadDir(layout, pin = labPin()) {
|
|
26
|
+
return join(layout.downloads, pin.release.sha256)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The ISO on disk against its verification record. The record is what
|
|
31
|
+
* `setup` wrote after the digest and the signature passed; the file is
|
|
32
|
+
* held to the record by size and mtime, which is what can be read in a
|
|
33
|
+
* millisecond, and `--verify` is the five-second re-hash.
|
|
34
|
+
*/
|
|
35
|
+
export function inspectDownload(layout, pin = labPin(), { verify = false, stagingRoot = layout.staging, onProgress } = {}) {
|
|
36
|
+
const dir = downloadDir(layout, pin)
|
|
37
|
+
const iso = join(dir, pin.release.fileName)
|
|
38
|
+
const out = { dir, iso, present: false, bytes: null, partial: null, record: readJson(join(dir, "verified.json")), verified: false, reason: null, sidecars: { checksum: existsSync(`${iso}.sha256`), signature: existsSync(`${iso}.sig`) } }
|
|
39
|
+
const part = `${iso}.part`
|
|
40
|
+
if (existsSync(part)) out.partial = statSync(part).size
|
|
41
|
+
if (!existsSync(iso)) {
|
|
42
|
+
out.reason = out.partial !== null ? `not there; a partial download of ${out.partial.toLocaleString("en-US")} of ${pin.release.bytes.toLocaleString("en-US")} B is waiting to resume` : "not there"
|
|
43
|
+
return out
|
|
44
|
+
}
|
|
45
|
+
const st = statSync(iso)
|
|
46
|
+
out.present = true
|
|
47
|
+
out.bytes = st.size
|
|
48
|
+
if (verify) {
|
|
49
|
+
const judged = judgeRelease({ file: iso, signature: out.sidecars.signature ? `${iso}.sig` : null, checksum: out.sidecars.checksum ? `${iso}.sha256` : null, pin, stagingRoot, onProgress })
|
|
50
|
+
out.verified = judged.ok
|
|
51
|
+
out.reason = judged.ok ? `verified now: ${judged.reason}` : `not verified: ${judged.reason}`
|
|
52
|
+
out.judged = judged
|
|
53
|
+
return out
|
|
54
|
+
}
|
|
55
|
+
if (!out.record) {
|
|
56
|
+
out.reason = `${st.size.toLocaleString("en-US")} B on disk with no verification record; \`omakit lab setup\` verifies it, or \`omakit lab inspect --verify\` checks it now`
|
|
57
|
+
return out
|
|
58
|
+
}
|
|
59
|
+
const same = out.record.bytes === st.size && out.record.sha256 === pin.release.sha256 && Math.abs(Number(out.record.mtimeMs) - st.mtimeMs) < 1
|
|
60
|
+
out.verified = same
|
|
61
|
+
out.reason = same
|
|
62
|
+
? `verified ${out.record.verifiedAt}: sha256 ${out.record.sha256}, signed by ${out.record.fingerprint}; the file's size and mtime still match that record`
|
|
63
|
+
: `the file changed since the record of ${out.record.verifiedAt} (${out.record.bytes === st.size ? "same size, different mtime" : `${st.size.toLocaleString("en-US")} B now, ${Number(out.record.bytes).toLocaleString("en-US")} B then`}); \`omakit lab setup\` verifies it again`
|
|
64
|
+
return out
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* The base: missing, ready, mismatch (a base of another release than the
|
|
69
|
+
* pin), or invalid (files without a complete manifest, or a disk whose
|
|
70
|
+
* size is not the recorded one). Never booted here. `allocatedBytes` is
|
|
71
|
+
* `du -B1` over the directory, the figure prune will recover.
|
|
72
|
+
*/
|
|
73
|
+
export function inspectBase(layout, pin = labPin()) {
|
|
74
|
+
const dir = layout.base
|
|
75
|
+
const out = { dir, state: "missing", reason: "no base; run `omakit lab setup`", manifest: null, allocatedBytes: 0, disk: join(dir, BASE_FILES.disk) }
|
|
76
|
+
if (!existsSync(dir)) return out
|
|
77
|
+
out.allocatedBytes = allocatedBytes(dir)
|
|
78
|
+
const manifest = readJson(join(dir, BASE_FILES.manifest))
|
|
79
|
+
const diskThere = existsSync(out.disk)
|
|
80
|
+
if (!manifest || manifest.state !== "ready" || !manifest.release || !manifest.guest || !manifest.disk) {
|
|
81
|
+
out.state = "invalid"
|
|
82
|
+
out.reason = diskThere ? `${BASE_FILES.disk} is there (${bytesBoth(out.allocatedBytes)}) but ${BASE_FILES.manifest} is ${manifest ? "incomplete" : "missing"}; it will not be booted` : `a base directory with no disk and ${manifest ? "an incomplete" : "no"} manifest; \`omakit lab prune\` removes it`
|
|
83
|
+
return out
|
|
84
|
+
}
|
|
85
|
+
out.manifest = manifest
|
|
86
|
+
if (!diskThere) {
|
|
87
|
+
out.state = "invalid"
|
|
88
|
+
out.reason = `the manifest names ${BASE_FILES.disk} and it is not there; it will not be booted`
|
|
89
|
+
return out
|
|
90
|
+
}
|
|
91
|
+
const size = statSync(out.disk).size
|
|
92
|
+
if (size !== manifest.disk.bytes) {
|
|
93
|
+
out.state = "invalid"
|
|
94
|
+
out.reason = `${BASE_FILES.disk} is ${size.toLocaleString("en-US")} B and the manifest recorded ${Number(manifest.disk.bytes).toLocaleString("en-US")} B; it will not be booted`
|
|
95
|
+
return out
|
|
96
|
+
}
|
|
97
|
+
for (const name of [BASE_FILES.vars, BASE_FILES.key]) {
|
|
98
|
+
if (!existsSync(join(dir, name))) {
|
|
99
|
+
out.state = "invalid"
|
|
100
|
+
out.reason = `${name} is missing from the base directory; it will not be booted`
|
|
101
|
+
return out
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (manifest.release.sha256 !== pin.release.sha256 || manifest.guest.version !== pin.release.expectedGuestVersion) {
|
|
105
|
+
out.state = "mismatch"
|
|
106
|
+
out.reason = `cached Omarchy ${manifest.release.name} (guest ${manifest.guest.version}, iso ${manifest.release.sha256}); required ${pin.release.name} (guest ${pin.release.expectedGuestVersion}, iso ${pin.release.sha256}); \`omakit lab setup\` replaces it`
|
|
107
|
+
return out
|
|
108
|
+
}
|
|
109
|
+
out.state = "ready"
|
|
110
|
+
out.reason = `Omarchy ${manifest.release.name}, guest omarchy ${manifest.guest.version}; ${bytesBoth(out.allocatedBytes)}; created ${manifest.createdAt}`
|
|
111
|
+
return out
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* The toolchain: the omarchy-iso checkout `setup --toolchain` recorded,
|
|
116
|
+
* whose harness must hash to the pinned patched digest before setup runs
|
|
117
|
+
* it. What is checked is the file, not git state: a harness that hashes
|
|
118
|
+
* to the pin is the pinned commit with the patch applied, whatever else
|
|
119
|
+
* the checkout holds.
|
|
120
|
+
*/
|
|
121
|
+
export function inspectToolchain(layout, pin = labPin()) {
|
|
122
|
+
const record = readJson(layout.toolchain)
|
|
123
|
+
const out = { record, dir: record?.dir || null, harness: null, sha256: null, state: "missing", reason: null }
|
|
124
|
+
if (!record?.dir) {
|
|
125
|
+
out.reason = "no toolchain recorded; `omakit lab setup --toolchain <omarchy-iso checkout>` records one"
|
|
126
|
+
return out
|
|
127
|
+
}
|
|
128
|
+
out.harness = join(record.dir, pin.toolchain.harness)
|
|
129
|
+
if (!existsSync(out.harness)) {
|
|
130
|
+
out.reason = `${out.harness} is not there (the recorded checkout moved or was removed)`
|
|
131
|
+
return out
|
|
132
|
+
}
|
|
133
|
+
out.sha256 = sha256File(out.harness).sha256
|
|
134
|
+
if (out.sha256 === pin.toolchain.patchedHarnessSha256) {
|
|
135
|
+
out.state = "ready"
|
|
136
|
+
out.reason = `${out.harness} hashes to the pinned patched harness (omarchy-iso ${pin.toolchain.commit.slice(0, 12)} with ${pin.toolchain.patch})`
|
|
137
|
+
} else if (out.sha256 === pin.toolchain.upstreamHarnessSha256) {
|
|
138
|
+
out.state = "unpatched"
|
|
139
|
+
out.reason = `${out.harness} is the upstream harness at ${pin.toolchain.commit.slice(0, 12)} without the patch; \`git -C ${record.dir} apply ${join(LAB_DIR, pin.toolchain.patch)}\``
|
|
140
|
+
} else {
|
|
141
|
+
out.state = "unknown"
|
|
142
|
+
out.reason = `${out.harness} hashes to ${out.sha256}, neither the pinned patched harness nor the upstream one at ${pin.toolchain.commit.slice(0, 12)}`
|
|
143
|
+
}
|
|
144
|
+
return out
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The one command that prepares a toolchain checkout, printed whole so it
|
|
149
|
+
* can be run as it stands. `dir` is under the resolved lab cache: with
|
|
150
|
+
* XDG_CACHE_HOME set, the remedy names that root, not ~/.cache (measured
|
|
151
|
+
* on 2026-09-19 by a first user under XDG_CACHE_HOME=/tmp/empty-cache,
|
|
152
|
+
* whose `lab inspect` printed clone and setup commands into ~/.cache;
|
|
153
|
+
* docs/evidence/ux/2026-09-19-first-user-test.json, finding 6).
|
|
154
|
+
*/
|
|
155
|
+
export function toolchainCommand(pin = labPin(), dir = join(labLayout().cache, "toolchain/omarchy-iso")) {
|
|
156
|
+
return `git clone ${pin.toolchain.repository} ${dir} && git -C ${dir} checkout --detach ${pin.toolchain.commit} && git -C ${dir} apply ${join(LAB_DIR, pin.toolchain.patch)} && omakit lab setup --toolchain ${dir}`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Staging directories, each with whether a QEMU still answers on its socket (the cross-namespace liveness question). */
|
|
160
|
+
export async function inspectStaging(layout) {
|
|
161
|
+
if (!existsSync(layout.staging)) return []
|
|
162
|
+
const entries = []
|
|
163
|
+
for (const name of readdirSync(layout.staging).sort()) {
|
|
164
|
+
const dir = join(layout.staging, name)
|
|
165
|
+
let st
|
|
166
|
+
try {
|
|
167
|
+
st = statSync(dir)
|
|
168
|
+
} catch {
|
|
169
|
+
continue
|
|
170
|
+
}
|
|
171
|
+
if (!st.isDirectory()) continue
|
|
172
|
+
const socket = join(dir, "qmp.sock")
|
|
173
|
+
const alive = existsSync(socket) ? (await qmpAlive(socket)).alive : false
|
|
174
|
+
entries.push({ name, dir, allocatedBytes: allocatedBytes(dir), alive, socket: existsSync(socket) ? socket : null })
|
|
175
|
+
}
|
|
176
|
+
return entries
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The lock: a directory with a record of the run holding it, and whether that run's QEMU still answers. */
|
|
180
|
+
export async function inspectLock(layout) {
|
|
181
|
+
const record = readJson(join(layout.lock, "holder.json"))
|
|
182
|
+
if (!existsSync(layout.lock)) return { held: false, record: null, alive: false }
|
|
183
|
+
const alive = record?.qmpSocket ? (await qmpAlive(record.qmpSocket)).alive : false
|
|
184
|
+
let pidAlive = false
|
|
185
|
+
try {
|
|
186
|
+
if (record?.pid) {
|
|
187
|
+
process.kill(record.pid, 0)
|
|
188
|
+
pidAlive = true
|
|
189
|
+
}
|
|
190
|
+
} catch {
|
|
191
|
+
pidAlive = false
|
|
192
|
+
}
|
|
193
|
+
return { held: true, record, alive: alive || pidAlive, qemuAnswers: alive, holderPidAlive: pidAlive }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Everything `inspect` and `doctor` print, as one document. `host` is the
|
|
198
|
+
* run probes plus what acquisition and a build need; `missing` is the list
|
|
199
|
+
* of what stands between this host and a run, each with its cost and the
|
|
200
|
+
* one command.
|
|
201
|
+
*/
|
|
202
|
+
export async function inspectLab({ env = process.env, pin = labPin(), verify = false, onProgress, run } = {}) {
|
|
203
|
+
const layout = labLayout(env)
|
|
204
|
+
const download = inspectDownload(layout, pin, { verify, onProgress })
|
|
205
|
+
const base = inspectBase(layout, pin)
|
|
206
|
+
const toolchain = inspectToolchain(layout, pin)
|
|
207
|
+
const staging = await inspectStaging(layout)
|
|
208
|
+
const lock = await inspectLock(layout)
|
|
209
|
+
const host = { run: probeRunHost({ pin, run, env }), verify: probeCommands(VERIFY_COMMANDS, { run, env }), build: probeCommands(BUILD_COMMANDS, { run, env }) }
|
|
210
|
+
const free = freeBytesAt(layout.cache)
|
|
211
|
+
const runs = existsSync(layout.runs) ? readdirSync(layout.runs).filter((name) => /^\d{8}-\d{6}/.test(name)).sort() : []
|
|
212
|
+
const totals = {
|
|
213
|
+
cacheBytes: existsSync(layout.cache) ? allocatedBytes(layout.cache) : 0,
|
|
214
|
+
downloadsBytes: existsSync(layout.downloads) ? allocatedBytes(layout.downloads) : 0,
|
|
215
|
+
baseBytes: base.allocatedBytes,
|
|
216
|
+
stagingBytes: staging.reduce((sum, entry) => sum + entry.allocatedBytes, 0),
|
|
217
|
+
pluginsBytes: existsSync(layout.plugins) ? allocatedBytes(layout.plugins) : 0,
|
|
218
|
+
runsBytes: existsSync(layout.runs) ? allocatedBytes(layout.runs) : 0,
|
|
219
|
+
}
|
|
220
|
+
const missing = []
|
|
221
|
+
for (const line of host.run) if (line.state !== "ok") missing.push({ what: line.name, cost: line.reason, command: line.remedy })
|
|
222
|
+
if (!download.verified) {
|
|
223
|
+
missing.push({
|
|
224
|
+
what: "the verified ISO",
|
|
225
|
+
cost: download.present ? `verification of the ${bytesBoth(download.bytes)} on disk (about ${durationWords(5215 + 9430)} on the reference host: the hash and the signature check)` : `a download of ${bytesBoth(pin.release.bytes)} from ${pin.release.isoUrl}, verified against the pinned sha256 and the Omarchy signature, into ${download.dir}`,
|
|
226
|
+
command: "omakit lab setup",
|
|
227
|
+
})
|
|
228
|
+
}
|
|
229
|
+
if (toolchain.state !== "ready" && base.state !== "ready") {
|
|
230
|
+
missing.push({ what: "the toolchain", cost: `a checkout of omarchy-iso at ${pin.toolchain.commit.slice(0, 12)} with ${pin.toolchain.patch} applied; it drives the installer once, to build the base`, command: toolchainCommand(pin, join(layout.cache, "toolchain/omarchy-iso")) })
|
|
231
|
+
}
|
|
232
|
+
if (base.state !== "ready") {
|
|
233
|
+
missing.push({
|
|
234
|
+
what: base.state === "missing" ? "a prepared base" : `a usable base (the one there is ${base.state})`,
|
|
235
|
+
cost: `${bytesBoth(pin.measured.baseDirectoryBytes)} on disk, ${durationWords(pin.measured.buildMilliseconds)} to build on the reference host (M14), plus one verification boot`,
|
|
236
|
+
command: "omakit lab setup",
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
return { pin: { release: pin.release, toolchain: pin.toolchain, guest: pin.guest, measured: pin.measured }, layout, download, base, toolchain, staging, lock, host, free, runs, totals, missing }
|
|
240
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
2
|
+
|
|
3
|
+
mDMEaLAsIhYJKwYBBAHaRw8BAQdAYmKmrWMlfpdIPh3QzvEMzzxdNd/kqDl/Bj8H
|
|
4
|
+
mCavKji0Gk9tYXJjaHkgPHBrZ3NAb21hcmNoeS5vcmc+iJAEExYKADgWIQRA37Yw
|
|
5
|
+
/0K8/7BHBGzwE07mgMrFcQUCaLAsIgIbAwULCQgHAgYVCgkICwIEFgIDAQIeAQIX
|
|
6
|
+
gAAKCRDwE07mgMrFcRF2AQCjjvjgju/kowkN69nenqqRnE+v2MrHmWh2wU0ugIt9
|
|
7
|
+
3gD/bXGugWc5J1vqoT+5aCnJAtfRRmHEfsrAiFgjduB+VA24OARosCwiEgorBgEE
|
|
8
|
+
AZdVAQUBAQdAJoU73WdgxE/pEYK5ofyRvQDs1IFRdfJHV9j4Jm+tuDUDAQgHiHgE
|
|
9
|
+
GBYKACAWIQRA37Yw/0K8/7BHBGzwE07mgMrFcQUCaLAsIgIbDAAKCRDwE07mgMrF
|
|
10
|
+
cYXsAQC5eBEGK0xKsGwtnDDVeNAapx32dpvLgJ94W4DtIpdLlwEA85cSsTiBMuGy
|
|
11
|
+
6kwYmHzqw7oFx3VYGXdMNl0SMKzvJA4=
|
|
12
|
+
=G7wU
|
|
13
|
+
-----END PGP PUBLIC KEY BLOCK-----
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
diff --git c/bin/omarchy-iso-test w/bin/omarchy-iso-test
|
|
2
|
+
index 281cf56..be30127 100755
|
|
3
|
+
--- c/bin/omarchy-iso-test
|
|
4
|
+
+++ w/bin/omarchy-iso-test
|
|
5
|
+
@@ -21,10 +21,18 @@
|
|
6
|
+
# --sync-omarchy DIR Push DIR's test/acceptance into the guest before running
|
|
7
|
+
# (defaults to OMARCHY_PATH when the suite is present)
|
|
8
|
+
# --sync-all DIR Push all of DIR (bin/config/shell/test) into the guest
|
|
9
|
+
+# --dev-link With --sync-all, start a fresh graphical session linked
|
|
10
|
+
+# to the checkout so the whole desktop uses local source
|
|
11
|
+
+# --host-test FILE Source FILE after boot; it must define omarchy_host_test
|
|
12
|
+
+# and may use the harness QMP/SSH/artifact helpers
|
|
13
|
+
+# --host-test-only Run only --host-test after source activation, skipping
|
|
14
|
+
+# the broad desktop and in-guest acceptance suites
|
|
15
|
+
# --port PORT Host port forwarded to guest SSH (default 2222)
|
|
16
|
+
# --memory MB VM memory (default 8192)
|
|
17
|
+
# --timeout SECS Install timeout (default 2400)
|
|
18
|
+
# --keep-running Leave the acceptance VM running after the suite finishes
|
|
19
|
+
+# --discard-overlay Delete the disposable run disk after the VM stops
|
|
20
|
+
+# --keep-overlay Retain the run disk (overrides --discard-overlay)
|
|
21
|
+
# --no-preview Do not open the collected screenshots in imv
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
@@ -42,12 +50,17 @@ PROVISION=false
|
|
25
|
+
REUSE_BASE=false
|
|
26
|
+
INSTALL_ONLY=false
|
|
27
|
+
KEEP_RUNNING=false
|
|
28
|
+
+DISCARD_OVERLAY=false
|
|
29
|
+
NO_PREVIEW=false
|
|
30
|
+
SYNC_DIR=""
|
|
31
|
+
SYNC_ALL=false
|
|
32
|
+
+DEV_LINK=false
|
|
33
|
+
+HOST_TEST=""
|
|
34
|
+
+HOST_TEST_ONLY=false
|
|
35
|
+
|
|
36
|
+
GUEST_USER="omarchy"
|
|
37
|
+
GUEST_PASSWORD="omarchy"
|
|
38
|
+
+GREETER_PATTERN='Opinionated\|Beautiful, Fun & Agentic Linux by DHH'
|
|
39
|
+
GUEST_HOSTNAME="omarchy-test"
|
|
40
|
+
|
|
41
|
+
while (($#)); do
|
|
42
|
+
@@ -57,9 +70,14 @@ while (($#)); do
|
|
43
|
+
--reuse-base) REUSE_BASE=true ;;
|
|
44
|
+
--install-only) INSTALL_ONLY=true ;;
|
|
45
|
+
--keep-running) KEEP_RUNNING=true ;;
|
|
46
|
+
+ --discard-overlay) DISCARD_OVERLAY=true ;;
|
|
47
|
+
+ --keep-overlay) DISCARD_OVERLAY=false ;;
|
|
48
|
+
--no-preview) NO_PREVIEW=true ;;
|
|
49
|
+
--sync-omarchy) SYNC_DIR="$2"; shift ;;
|
|
50
|
+
--sync-all) SYNC_DIR="$2"; SYNC_ALL=true; shift ;;
|
|
51
|
+
+ --dev-link) DEV_LINK=true ;;
|
|
52
|
+
+ --host-test) HOST_TEST="$2"; shift ;;
|
|
53
|
+
+ --host-test-only) HOST_TEST_ONLY=true ;;
|
|
54
|
+
--port) SSH_PORT="$2"; shift ;;
|
|
55
|
+
--memory) MEMORY="$2"; shift ;;
|
|
56
|
+
--timeout) INSTALL_TIMEOUT="$2"; shift ;;
|
|
57
|
+
@@ -69,6 +87,23 @@ while (($#)); do
|
|
58
|
+
shift
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
+if $DEV_LINK && ! $SYNC_ALL; then
|
|
62
|
+
+ echo "--dev-link requires --sync-all DIR" >&2
|
|
63
|
+
+ exit 1
|
|
64
|
+
+fi
|
|
65
|
+
+
|
|
66
|
+
+if [[ -n $HOST_TEST ]]; then
|
|
67
|
+
+ HOST_TEST=$(realpath -e "$HOST_TEST") || {
|
|
68
|
+
+ echo "Host test not found: $HOST_TEST" >&2
|
|
69
|
+
+ exit 1
|
|
70
|
+
+ }
|
|
71
|
+
+fi
|
|
72
|
+
+
|
|
73
|
+
+if $HOST_TEST_ONLY && [[ -z $HOST_TEST ]]; then
|
|
74
|
+
+ echo "--host-test-only requires --host-test FILE" >&2
|
|
75
|
+
+ exit 1
|
|
76
|
+
+fi
|
|
77
|
+
+
|
|
78
|
+
if [[ -z $SYNC_DIR && -f ${OMARCHY_PATH:-}/test/acceptance ]]; then
|
|
79
|
+
SYNC_DIR="$OMARCHY_PATH"
|
|
80
|
+
fi
|
|
81
|
+
@@ -83,10 +118,21 @@ if [[ ! -f ${ISO:-} ]]; then
|
|
82
|
+
exit 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
-omarchy-pkg-add qemu-full edk2-ovmf socat imagemagick tesseract tesseract-data-eng
|
|
86
|
+
+for required_command in qemu-system-x86_64 qemu-img socat magick tesseract; do
|
|
87
|
+
+ if ! command -v "$required_command" >/dev/null; then
|
|
88
|
+
+ echo "Missing host prerequisite: $required_command" >&2
|
|
89
|
+
+ exit 1
|
|
90
|
+
+ fi
|
|
91
|
+
+done
|
|
92
|
+
|
|
93
|
+
OVMF_CODE="/usr/share/edk2/x64/OVMF_CODE.4m.fd"
|
|
94
|
+
OVMF_VARS_TEMPLATE="/usr/share/edk2/x64/OVMF_VARS.4m.fd"
|
|
95
|
+
+for firmware in "$OVMF_CODE" "$OVMF_VARS_TEMPLATE"; do
|
|
96
|
+
+ if [[ ! -r $firmware ]]; then
|
|
97
|
+
+ echo "Missing readable UEFI firmware: $firmware" >&2
|
|
98
|
+
+ exit 1
|
|
99
|
+
+ fi
|
|
100
|
+
+done
|
|
101
|
+
|
|
102
|
+
BASE_NAME="$(basename "$ISO" .iso)"
|
|
103
|
+
$ENCRYPT && BASE_NAME+="-encrypted"
|
|
104
|
+
@@ -121,6 +167,52 @@ qmp() {
|
|
105
|
+
timeout 5 socat -t 2 - "UNIX-CONNECT:$QMP_SOCK" 2>/dev/null || true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
+qmp_pointer_move() {
|
|
109
|
+
+ local width="$1" height="$2" x="$3" y="$4" qx qy response
|
|
110
|
+
+
|
|
111
|
+
+ if ((width < 2 || height < 2 || x < 0 || y < 0 || x >= width || y >= height)); then
|
|
112
|
+
+ printf 'invalid QMP pointer geometry: viewport=%sx%s point=%s,%s\n' \
|
|
113
|
+
+ "$width" "$height" "$x" "$y" >&2
|
|
114
|
+
+ return 2
|
|
115
|
+
+ fi
|
|
116
|
+
+ qx=$((x * 32767 / (width - 1)))
|
|
117
|
+
+ qy=$((y * 32767 / (height - 1)))
|
|
118
|
+
+ response=$(qmp "\"input-send-event\", \"arguments\": {\"events\": [
|
|
119
|
+
+ {\"type\":\"abs\",\"data\":{\"axis\":\"x\",\"value\":$qx}},
|
|
120
|
+
+ {\"type\":\"abs\",\"data\":{\"axis\":\"y\",\"value\":$qy}}
|
|
121
|
+
+ ]}")
|
|
122
|
+
+ if grep -q '"error"' <<<"$response"; then
|
|
123
|
+
+ printf 'QMP pointer move failed: %s\n' "$response" >&2
|
|
124
|
+
+ return 1
|
|
125
|
+
+ fi
|
|
126
|
+
+}
|
|
127
|
+
+
|
|
128
|
+
+qmp_pointer_tap() {
|
|
129
|
+
+ local width="$1" height="$2" x="$3" y="$4" button="${5:-left}" response
|
|
130
|
+
+
|
|
131
|
+
+ case $button in
|
|
132
|
+
+ left | right | middle) ;;
|
|
133
|
+
+ *)
|
|
134
|
+
+ printf 'invalid QMP pointer button: %s\n' "$button" >&2
|
|
135
|
+
+ return 2
|
|
136
|
+
+ ;;
|
|
137
|
+
+ esac
|
|
138
|
+
+ qmp_pointer_move "$width" "$height" 1 1
|
|
139
|
+
+ sleep 0.1
|
|
140
|
+
+ qmp_pointer_move "$width" "$height" "$x" "$y"
|
|
141
|
+
+ sleep 0.1
|
|
142
|
+
+ response=$(qmp "\"input-send-event\", \"arguments\": {\"events\": [
|
|
143
|
+
+ {\"type\":\"btn\",\"data\":{\"down\":true,\"button\":\"$button\"}}
|
|
144
|
+
+ ]}")
|
|
145
|
+
+ grep -q '"error"' <<<"$response" && return 1
|
|
146
|
+
+ sleep 0.12
|
|
147
|
+
+ response=$(qmp "\"input-send-event\", \"arguments\": {\"events\": [
|
|
148
|
+
+ {\"type\":\"btn\",\"data\":{\"down\":false,\"button\":\"$button\"}}
|
|
149
|
+
+ ]}")
|
|
150
|
+
+ grep -q '"error"' <<<"$response" && return 1
|
|
151
|
+
+ sleep 0.4
|
|
152
|
+
+}
|
|
153
|
+
+
|
|
154
|
+
screendump() {
|
|
155
|
+
qmp "\"screendump\", \"arguments\": {\"filename\": \"$1\"}" >/dev/null
|
|
156
|
+
}
|
|
157
|
+
@@ -188,6 +280,15 @@ cleanup() {
|
|
158
|
+
if ! $KEEP_RUNNING; then
|
|
159
|
+
vm_running && log "Stopping test VM"
|
|
160
|
+
stop_vm
|
|
161
|
+
+
|
|
162
|
+
+ if $DISCARD_OVERLAY && [[ -f $RUN_DIR/run.qcow2 ]]; then
|
|
163
|
+
+ if vm_running; then
|
|
164
|
+
+ echo "WARN: VM did not stop; preserving its run overlay: $RUN_DIR/run.qcow2" >&2
|
|
165
|
+
+ else
|
|
166
|
+
+ rm -f -- "$RUN_DIR/run.qcow2"
|
|
167
|
+
+ log "Discarded the stopped VM overlay; run evidence remains in $RUN_DIR"
|
|
168
|
+
+ fi
|
|
169
|
+
+ fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
rm -f "$RUN_DIR/.screen.ppm" "$RUN_DIR/.screen.png"
|
|
173
|
+
@@ -355,7 +456,10 @@ wait_for_ssh() {
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
session_started() {
|
|
177
|
+
- ssh_guest "ls /run/user/1000/hypr 2>/dev/null | grep -q ." 2>/dev/null
|
|
178
|
+
+ # Hyprland leaves its runtime directory behind briefly after logout, while
|
|
179
|
+
+ # SDDM starts a different compositor as the sddm user. Only the compositor
|
|
180
|
+
+ # owned by the test user proves that the Omarchy session is actually ready.
|
|
181
|
+
+ ssh_guest "pgrep -u \$(id -u) -x Hyprland >/dev/null" 2>/dev/null
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
ssh_session() {
|
|
185
|
+
@@ -367,7 +471,8 @@ ssh_session() {
|
|
186
|
+
export LANG=\${LANG:-C.UTF-8}; \
|
|
187
|
+
export HYPRLAND_INSTANCE_SIGNATURE=\$(ls -t \$XDG_RUNTIME_DIR/hypr | head -1); \
|
|
188
|
+
export WAYLAND_DISPLAY=\$(find \$XDG_RUNTIME_DIR -maxdepth 1 -name 'wayland-*' ! -name '*.lock' -printf '%f\\n' | head -1); \
|
|
189
|
+
- export OMARCHY_PATH=/usr/share/omarchy; \
|
|
190
|
+
+ [[ ! -f /etc/omarchy.conf ]] || source /etc/omarchy.conf; \
|
|
191
|
+
+ export OMARCHY_PATH=\${OMARCHY_PATH:-/usr/share/omarchy}; \
|
|
192
|
+
export PATH=\$OMARCHY_PATH/bin:\$PATH; \
|
|
193
|
+
$command" </dev/null
|
|
194
|
+
}
|
|
195
|
+
@@ -399,6 +504,20 @@ guest_layer_absent() {
|
|
196
|
+
! guest_layer_present "$1"
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
+session_stopped() {
|
|
200
|
+
+ ! session_started
|
|
201
|
+
+}
|
|
202
|
+
+
|
|
203
|
+
+clear_startup_notifications() {
|
|
204
|
+
+ [[ -n $HOST_TEST ]] || return 0
|
|
205
|
+
+
|
|
206
|
+
+ wait_for_guest_state "notification service is ready" 15 \
|
|
207
|
+
+ ssh_session "omarchy-shell notifications ping >/dev/null" || return 1
|
|
208
|
+
+ ssh_session "omarchy-shell notifications dismissAll >/dev/null" || return 1
|
|
209
|
+
+ wait_for_guest_state "startup notifications are dismissed" 15 \
|
|
210
|
+
+ guest_layer_absent "omarchy-notifications"
|
|
211
|
+
+}
|
|
212
|
+
+
|
|
213
|
+
cleanup_shortcut_state() {
|
|
214
|
+
ssh_session 'for plugin in omarchy.launcher omarchy.menu omarchy.emojis omarchy.clipboard omarchy.audio; do
|
|
215
|
+
omarchy-shell shell hide "$plugin" >/dev/null 2>&1 || true
|
|
216
|
+
@@ -607,7 +726,10 @@ drive_configurator() {
|
|
217
|
+
log "Driving the installer wizard"
|
|
218
|
+
|
|
219
|
+
# A greeter (animated logo + tagline) opens the installer; Return begins.
|
|
220
|
+
- wait_for_screen "Opinionated" 300
|
|
221
|
+
+ # Released installers before v4.0.3 use the original slogan; v4.0.3 uses
|
|
222
|
+
+ # the exact replacement below. Keep this allowlist explicit so unrelated
|
|
223
|
+
+ # OCR text cannot be mistaken for an installer-ready signal.
|
|
224
|
+
+ wait_for_screen "$GREETER_PATTERN" 300
|
|
225
|
+
capture_console "success-installer-00-greeter"
|
|
226
|
+
press ret
|
|
227
|
+
|
|
228
|
+
@@ -631,7 +753,10 @@ drive_configurator() {
|
|
229
|
+
capture_console "success-installer-04-prepare-disk-warning-encrypted"
|
|
230
|
+
if ! $ENCRYPT; then
|
|
231
|
+
press ctrl-c
|
|
232
|
+
- wait_for_screen "without encryption" 30
|
|
233
|
+
+ # The selected confirmation label is light text on a light background,
|
|
234
|
+
+ # which Tesseract intentionally loses. The prompt itself stays put, so
|
|
235
|
+
+ # give the Ctrl+C state change time to render and capture it as proof.
|
|
236
|
+
+ sleep 2
|
|
237
|
+
capture_console "success-installer-05-prepare-disk-warning-unencrypted"
|
|
238
|
+
fi
|
|
239
|
+
press ret
|
|
240
|
+
@@ -695,7 +820,10 @@ drive_configurator() {
|
|
241
|
+
capture_console "success-installer-12-disk-warning-encrypted"
|
|
242
|
+
if ! $ENCRYPT; then
|
|
243
|
+
press ctrl-c # toggles the confirm to the unencrypted flow
|
|
244
|
+
- wait_for_screen "without encryption" 30
|
|
245
|
+
+ # The selected confirmation label is light text on a light background,
|
|
246
|
+
+ # which Tesseract intentionally loses. The prompt itself stays put, so
|
|
247
|
+
+ # give the Ctrl+C state change time to render and capture it as proof.
|
|
248
|
+
+ sleep 2
|
|
249
|
+
capture_console "success-installer-13-disk-warning-unencrypted"
|
|
250
|
+
fi
|
|
251
|
+
press ret
|
|
252
|
+
@@ -716,7 +844,7 @@ drive_provision_owner() {
|
|
253
|
+
# layout — UK leaves every letter identical to US, so the typed password (sent
|
|
254
|
+
# as raw keycodes) still matches the literal one the harness pipes to sudo.
|
|
255
|
+
# A greeter (animated logo + tagline) opens first boot; Return begins.
|
|
256
|
+
- wait_for_screen "Opinionated" 600
|
|
257
|
+
+ wait_for_screen "$GREETER_PATTERN" 600
|
|
258
|
+
capture_console "success-provision-00-greeter"
|
|
259
|
+
press ret
|
|
260
|
+
|
|
261
|
+
@@ -791,7 +919,7 @@ wait_for_install() {
|
|
262
|
+
# Deferred-provisioning installs skip the celebration and reboot on their own
|
|
263
|
+
# (no Reboot Now prompt); the first-boot greeter tagline appearing means the
|
|
264
|
+
# install finished and rebooted into setup.
|
|
265
|
+
- if $PROVISION && grep -qi "Opinionated" <<<"$text"; then
|
|
266
|
+
+ if $PROVISION && grep -qi "$GREETER_PATTERN" <<<"$text"; then
|
|
267
|
+
log "Install finished and auto-rebooted into first-boot setup."
|
|
268
|
+
capture_console "success-installer-15-autoreboot"
|
|
269
|
+
return 0
|
|
270
|
+
@@ -1063,6 +1191,49 @@ sync_omarchy() {
|
|
271
|
+
fi
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
+activate_dev_checkout() {
|
|
275
|
+
+ $DEV_LINK || return 0
|
|
276
|
+
+
|
|
277
|
+
+ local target=".local/share/omarchy"
|
|
278
|
+
+ log "Linking the disposable guest session to the synced Omarchy checkout"
|
|
279
|
+
+
|
|
280
|
+
+ ssh_guest "echo '$GUEST_PASSWORD' | sudo -S -v && $target/bin/omarchy-dev-link $target --no-reboot"
|
|
281
|
+
+
|
|
282
|
+
+ # omarchy-dev-link deliberately leaves the running UWSM environment alone.
|
|
283
|
+
+ # A fresh graphical login is enough to rebuild that environment from
|
|
284
|
+
+ # /etc/omarchy.conf. Avoid rebooting here: the acceptance disk is already a
|
|
285
|
+
+ # throwaway overlay, and some released unencrypted ISOs have a separate
|
|
286
|
+
+ # post-install Limine/resume defect that would obscure source-level tests.
|
|
287
|
+
+ ssh_guest "uwsm stop" >/dev/null 2>&1 || true
|
|
288
|
+
+
|
|
289
|
+
+ if ! wait_for_guest_state "linked graphical session stops" 60 session_stopped; then
|
|
290
|
+
+ return 1
|
|
291
|
+
+ fi
|
|
292
|
+
+
|
|
293
|
+
+ establish_session
|
|
294
|
+
+ ssh_guest "grep -Fq '$target' /etc/omarchy.conf"
|
|
295
|
+
+ log "Disposable guest is running entirely from the synced checkout"
|
|
296
|
+
+}
|
|
297
|
+
+
|
|
298
|
+
+run_host_test() {
|
|
299
|
+
+ [[ -n $HOST_TEST ]] || return 0
|
|
300
|
+
+
|
|
301
|
+
+ log "Running host-driven acceptance: $HOST_TEST"
|
|
302
|
+
+ # The host test is trusted local test code sourced deliberately so it can use
|
|
303
|
+
+ # press, ssh_guest, ssh_session, capture_console, wait_for_guest_state, and
|
|
304
|
+
+ # the current RUN_DIR without duplicating the QMP/SSH harness.
|
|
305
|
+
+ source "$HOST_TEST"
|
|
306
|
+
+ declare -F omarchy_host_test >/dev/null || {
|
|
307
|
+
+ echo "$HOST_TEST must define omarchy_host_test()" >&2
|
|
308
|
+
+ return 1
|
|
309
|
+
+ }
|
|
310
|
+
+ set +e
|
|
311
|
+
+ omarchy_host_test 2>&1 | tee "$RUN_DIR/host-test.log"
|
|
312
|
+
+ local status=${PIPESTATUS[0]}
|
|
313
|
+
+ set -e
|
|
314
|
+
+ return "$status"
|
|
315
|
+
+}
|
|
316
|
+
+
|
|
317
|
+
acceptance_phase() {
|
|
318
|
+
log "Booting acceptance VM from base image overlay"
|
|
319
|
+
|
|
320
|
+
@@ -1074,15 +1245,26 @@ acceptance_phase() {
|
|
321
|
+
|
|
322
|
+
establish_session
|
|
323
|
+
sync_omarchy
|
|
324
|
+
+ activate_dev_checkout
|
|
325
|
+
+ if ! clear_startup_notifications; then
|
|
326
|
+
+ capture_console "failure-startup-notifications"
|
|
327
|
+
+ return 1
|
|
328
|
+
+ fi
|
|
329
|
+
|
|
330
|
+
local status=0
|
|
331
|
+
- shortcut_smoke_phase || status=1
|
|
332
|
+
|
|
333
|
+
- log "Running acceptance suite"
|
|
334
|
+
- ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
|
|
335
|
+
+ if $HOST_TEST_ONLY; then
|
|
336
|
+
+ run_host_test || status=1
|
|
337
|
+
+ else
|
|
338
|
+
+ shortcut_smoke_phase || status=1
|
|
339
|
+
+ run_host_test || status=1
|
|
340
|
+
|
|
341
|
+
- log "Collecting artifacts into $RUN_DIR"
|
|
342
|
+
- ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
|
|
343
|
+
+ log "Running acceptance suite"
|
|
344
|
+
+ ssh_guest "OMARCHY_PATH=$guest_omarchy_path OMARCHY_ACCEPTANCE_DIR=/tmp/omarchy-acceptance bash .local/share/omarchy/test/acceptance" || status=$?
|
|
345
|
+
+
|
|
346
|
+
+ log "Collecting artifacts into $RUN_DIR"
|
|
347
|
+
+ ssh_guest "tar -C /tmp -cf - omarchy-acceptance 2>/dev/null" | tar -C "$RUN_DIR" -xf - || true
|
|
348
|
+
+ fi
|
|
349
|
+
ssh_guest "echo $GUEST_PASSWORD | sudo -S cat /var/log/omarchy-install.log 2>/dev/null" >"$RUN_DIR/omarchy-install.log" 2>/dev/null || true
|
|
350
|
+
if ((status == 0)); then
|
|
351
|
+
capture_console "success-acceptance-final"
|