@homeflare/alchemy 0.19.1 → 0.21.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/README.md +25 -0
- package/dist/caddy/access-forward-auth.d.ts +56 -0
- package/dist/caddy/access-forward-auth.d.ts.map +1 -0
- package/dist/caddy/index.d.ts +6 -0
- package/dist/caddy/index.d.ts.map +1 -1
- package/dist/caddy/index.js +169 -75
- package/dist/caddy/index.js.map +10 -7
- package/dist/cloudflare/mcp-server-api.d.ts +135 -0
- package/dist/cloudflare/mcp-server-api.d.ts.map +1 -0
- package/dist/cloudflare/mcp-server-form.d.ts +116 -0
- package/dist/cloudflare/mcp-server-form.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/launchd/file-converge.d.ts +70 -0
- package/dist/launchd/file-converge.d.ts.map +1 -0
- package/dist/launchd/file-identity.d.ts +32 -0
- package/dist/launchd/file-identity.d.ts.map +1 -0
- package/dist/launchd/host-file-form.d.ts +2 -0
- package/dist/launchd/host-file-form.d.ts.map +1 -1
- package/dist/launchd/host-file-lifecycle.d.ts +11 -24
- package/dist/launchd/host-file-lifecycle.d.ts.map +1 -1
- package/dist/launchd/host-file.d.ts.map +1 -1
- package/dist/launchd/index.js +173 -120
- package/dist/launchd/index.js.map +12 -10
- package/dist/launchd/local-runner.d.ts.map +1 -1
- package/dist/launchd/runner.d.ts +17 -2
- package/dist/launchd/runner.d.ts.map +1 -1
- package/dist/launchd/sudo-runner.d.ts +11 -0
- package/dist/launchd/sudo-runner.d.ts.map +1 -1
- package/dist/linux/index.js +37 -11
- package/dist/linux/index.js.map +10 -10
- package/dist/linux/region.d.ts +11 -0
- package/dist/linux/region.d.ts.map +1 -1
- package/dist/linux/remote-file-lifecycle.d.ts.map +1 -1
- package/dist/linux/remote-file-plan.d.ts +22 -0
- package/dist/linux/remote-file-plan.d.ts.map +1 -1
- package/dist/linux/remote-file.d.ts +4 -1
- package/dist/linux/remote-file.d.ts.map +1 -1
- package/dist/linux/sudo-listing.d.ts +47 -0
- package/dist/linux/sudo-listing.d.ts.map +1 -0
- package/dist/linux/unit-handlers.d.ts.map +1 -1
- package/dist/linux/unit-preflight.d.ts +10 -1
- package/dist/linux/unit-preflight.d.ts.map +1 -1
- package/dist/proxmox/credential-literals.d.ts +1 -1
- package/dist/proxmox/credential-literals.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs/nodes-node.d.ts +1 -1
- package/dist/proxmox/generated/pbs/nodes-node.d.ts.map +1 -1
- package/dist/proxmox/generated/pbs.d.ts +7 -0
- package/dist/proxmox/generated/pbs.d.ts.map +1 -1
- package/dist/proxmox/generated/pve.d.ts +7 -0
- package/dist/proxmox/generated/pve.d.ts.map +1 -1
- package/dist/proxmox/index.d.ts +16 -4
- package/dist/proxmox/index.d.ts.map +1 -1
- package/dist/proxmox/index.js +5 -2
- package/dist/proxmox/index.js.map +9 -9
- package/dist/proxmox/pbs-notification-target-form.d.ts +1 -1
- package/dist/proxmox/pbs-notification-target-form.d.ts.map +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts +1 -1
- package/dist/proxmox/pbs-notification-target.d.ts.map +1 -1
- package/dist/release/archive.d.ts +11 -0
- package/dist/release/archive.d.ts.map +1 -0
- package/dist/release/binary-claim.d.ts +53 -0
- package/dist/release/binary-claim.d.ts.map +1 -0
- package/dist/release/binary-diff.d.ts +38 -0
- package/dist/release/binary-diff.d.ts.map +1 -0
- package/dist/release/binary-form.d.ts +118 -0
- package/dist/release/binary-form.d.ts.map +1 -0
- package/dist/release/binary-lifecycle.d.ts +55 -0
- package/dist/release/binary-lifecycle.d.ts.map +1 -0
- package/dist/release/binary-read.d.ts +11 -0
- package/dist/release/binary-read.d.ts.map +1 -0
- package/dist/release/binary.d.ts +68 -0
- package/dist/release/binary.d.ts.map +1 -0
- package/dist/release/catalog.d.ts +76 -0
- package/dist/release/catalog.d.ts.map +1 -0
- package/dist/release/checksums.d.ts +22 -0
- package/dist/release/checksums.d.ts.map +1 -0
- package/dist/release/declared-pins.d.ts +4 -0
- package/dist/release/declared-pins.d.ts.map +1 -0
- package/dist/release/download.d.ts +44 -0
- package/dist/release/download.d.ts.map +1 -0
- package/dist/release/fake-deploy.d.ts +48 -0
- package/dist/release/fake-deploy.d.ts.map +1 -0
- package/dist/release/fake-engine.d.ts +8 -0
- package/dist/release/fake-engine.d.ts.map +1 -0
- package/dist/release/fake-install.d.ts +39 -0
- package/dist/release/fake-install.d.ts.map +1 -0
- package/dist/release/fake-provider.d.ts +58 -0
- package/dist/release/fake-provider.d.ts.map +1 -0
- package/dist/release/fake-release.d.ts +95 -0
- package/dist/release/fake-release.d.ts.map +1 -0
- package/dist/release/index.d.ts +24 -0
- package/dist/release/index.d.ts.map +1 -0
- package/dist/release/index.js +1365 -0
- package/dist/release/index.js.map +35 -0
- package/dist/release/providers.d.ts +19 -0
- package/dist/release/providers.d.ts.map +1 -0
- package/dist/release/refused.d.ts +39 -0
- package/dist/release/refused.d.ts.map +1 -0
- package/dist/release/tar.d.ts +14 -0
- package/dist/release/tar.d.ts.map +1 -0
- package/dist/release/victoria.d.ts +36 -0
- package/dist/release/victoria.d.ts.map +1 -0
- package/dist/{proxmox → secrets}/write-only.d.ts.map +1 -1
- package/docs/adopted-deploys.md +13 -0
- package/docs/linux-host.md +5 -1
- package/docs/provider-standard.md +138 -0
- package/docs/release-binary-catalogs.md +113 -0
- package/docs/release-binary-measured.md +163 -0
- package/docs/release-binary-refusals.md +57 -0
- package/docs/release-binary-upstream.md +163 -0
- package/docs/release-binary.md +154 -0
- package/docs/upstream-conformance.md +164 -0
- package/package.json +5 -1
- package/src/caddy/access-forward-auth.test.ts +63 -0
- package/src/caddy/access-forward-auth.ts +120 -0
- package/src/caddy/index.ts +10 -0
- package/src/caddy/providers.test.ts +4 -0
- package/src/cloudflare/mcp-server-api.ts +294 -0
- package/src/cloudflare/mcp-server-form.ts +219 -0
- package/src/index.ts +1 -0
- package/src/launchd/file-converge.ts +233 -0
- package/src/launchd/file-identity.ts +69 -0
- package/src/launchd/host-file-alias.test.ts +65 -0
- package/src/launchd/host-file-form.ts +2 -1
- package/src/launchd/host-file-lifecycle.ts +25 -144
- package/src/launchd/host-file.ts +8 -2
- package/src/launchd/local-runner.test.ts +12 -1
- package/src/launchd/local-runner.ts +2 -0
- package/src/launchd/runner.ts +17 -2
- package/src/launchd/sudo-runner.ts +11 -0
- package/src/linux/region.ts +15 -0
- package/src/linux/remote-file-lifecycle.test.ts +0 -88
- package/src/linux/remote-file-lifecycle.ts +25 -7
- package/src/linux/remote-file-plan.ts +37 -1
- package/src/linux/remote-file-region.test.ts +181 -0
- package/src/linux/remote-file.ts +4 -1
- package/src/linux/sudo-listing.ts +122 -0
- package/src/linux/unit-handlers.ts +12 -5
- package/src/linux/unit-preflight.ts +20 -4
- package/src/linux/unit-rename.test.ts +76 -0
- package/src/proxmox/ceph-adopt.test.ts +210 -0
- package/src/proxmox/credential-literals.ts +1 -1
- package/src/proxmox/generated/pbs/nodes-node.ts +1 -1
- package/src/proxmox/generated/pbs.ts +7 -0
- package/src/proxmox/generated/pve.ts +7 -0
- package/src/proxmox/index.test.ts +7 -4
- package/src/proxmox/index.ts +16 -4
- package/src/proxmox/pbs-notification-target-form.ts +1 -1
- package/src/proxmox/pbs-notification-target-lifecycle.ts +1 -1
- package/src/proxmox/pbs-notification-target-wire.ts +1 -1
- package/src/proxmox/pbs-notification-target.ts +1 -1
- package/src/release/adopt-parity.test.ts +106 -0
- package/src/release/apply-pins.test.ts +128 -0
- package/src/release/archive.ts +94 -0
- package/src/release/binary-alias.test.ts +109 -0
- package/src/release/binary-claim.ts +80 -0
- package/src/release/binary-claims.test.ts +47 -0
- package/src/release/binary-diff.ts +73 -0
- package/src/release/binary-form.ts +229 -0
- package/src/release/binary-lifecycle.test.ts +90 -0
- package/src/release/binary-lifecycle.ts +202 -0
- package/src/release/binary-read.ts +55 -0
- package/src/release/binary-refusals.test.ts +192 -0
- package/src/release/binary-sudo.test.ts +93 -0
- package/src/release/binary.ts +181 -0
- package/src/release/catalog.test.ts +96 -0
- package/src/release/catalog.ts +175 -0
- package/src/release/checksums.test.ts +35 -0
- package/src/release/checksums.ts +43 -0
- package/src/release/declared-pins.ts +39 -0
- package/src/release/download.test.ts +138 -0
- package/src/release/download.ts +110 -0
- package/src/release/error-channel.test.ts +69 -0
- package/src/release/fake-deploy.ts +87 -0
- package/src/release/fake-engine.ts +50 -0
- package/src/release/fake-install.ts +59 -0
- package/src/release/fake-provider.ts +68 -0
- package/src/release/fake-release.ts +185 -0
- package/src/release/fixtures/victoria/darwin-arm64-assets.txt +22 -0
- package/src/release/fixtures/victoria/victoria-logs-darwin-arm64-v1.52.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/victoria-metrics-darwin-arm64-v1.151.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/victoria-traces-darwin-arm64-v0.10.0_checksums.txt +2 -0
- package/src/release/fixtures/victoria/vmutils-darwin-arm64-v1.151.0_checksums.txt +8 -0
- package/src/release/index.ts +41 -0
- package/src/release/plan.test.ts +189 -0
- package/src/release/provider-refusals.test.ts +48 -0
- package/src/release/providers.test.ts +174 -0
- package/src/release/providers.ts +24 -0
- package/src/release/recovery-read.test.ts +44 -0
- package/src/release/refused.ts +40 -0
- package/src/release/tar.test.ts +142 -0
- package/src/release/tar.ts +195 -0
- package/src/release/victoria.test.ts +108 -0
- package/src/release/victoria.ts +154 -0
- package/src/{proxmox → secrets}/write-only.ts +6 -0
- /package/dist/{proxmox → secrets}/write-only.d.ts +0 -0
- /package/src/{proxmox → secrets}/write-only.test.ts +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Every refusal of the install lifecycle, each asserted by what it LEFT (fake-install.ts): the
|
|
3
|
+
* transport's request log, the host's write log, and the host's paths before and after.
|
|
4
|
+
*/
|
|
5
|
+
import { describe, expect, test } from 'bun:test';
|
|
6
|
+
import { sha256Hex } from '../launchd/job-form.ts';
|
|
7
|
+
import type { ReleaseBinaryProps } from './binary-form.ts';
|
|
8
|
+
import { reconcileBinary } from './binary-lifecycle.ts';
|
|
9
|
+
import {
|
|
10
|
+
BINARY,
|
|
11
|
+
VMUTILS_DIR,
|
|
12
|
+
VMUTILS_ENTRIES,
|
|
13
|
+
VMUTILS_URL,
|
|
14
|
+
bytesOf,
|
|
15
|
+
fakeTransport,
|
|
16
|
+
gzip,
|
|
17
|
+
pathsOn,
|
|
18
|
+
tarOf,
|
|
19
|
+
} from './fake-release.ts';
|
|
20
|
+
import { PATH, refused, setup } from './fake-install.ts';
|
|
21
|
+
import { ArchiveRefused, BinaryRefused, ChecksumMismatch, DownloadFailed } from './refused.ts';
|
|
22
|
+
|
|
23
|
+
describe('refusals leave the host exactly as it was', () => {
|
|
24
|
+
test('a wrong archive hash', async () => {
|
|
25
|
+
const s = setup();
|
|
26
|
+
const tampered = gzip(tarOf(VMUTILS_ENTRIES.map((e) => ({ ...e, bytes: bytesOf('evil') }))));
|
|
27
|
+
const transport = fakeTransport({ [VMUTILS_URL]: tampered });
|
|
28
|
+
const error = await refused(s, () =>
|
|
29
|
+
reconcileBinary(s.fake.runner, transport.fetch, s.props()),
|
|
30
|
+
);
|
|
31
|
+
expect(error).toBeInstanceOf(ChecksumMismatch);
|
|
32
|
+
expect(error.message).toContain(`Release.Binary ${PATH}: ${VMUTILS_URL} hashes to`);
|
|
33
|
+
expect(error.message).toContain('Nothing was written.');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('the right archive with a wrong binary pin', async () => {
|
|
37
|
+
const s = setup();
|
|
38
|
+
const error = await refused(s, () => s.install(s.props({ sha256: 'f'.repeat(64) })));
|
|
39
|
+
expect(error).toBeInstanceOf(ChecksumMismatch);
|
|
40
|
+
expect(error.message).toContain(`member "vmalert-prod" hashes to ${sha256Hex(BINARY.vmalert)}`);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('a tar-slip entry beside the declared member, in a correctly pinned archive', async () => {
|
|
44
|
+
const s = setup([
|
|
45
|
+
...VMUTILS_ENTRIES,
|
|
46
|
+
{ bytes: bytesOf('pwn'), name: '../../../etc/sudoers.d/x' },
|
|
47
|
+
]);
|
|
48
|
+
const error = await refused(s, () => s.install());
|
|
49
|
+
expect(error).toBeInstanceOf(ArchiveRefused);
|
|
50
|
+
expect(error.message).toContain('climbs out');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('the declared member as a symlink', async () => {
|
|
54
|
+
const s = setup([{ name: 'vmalert-prod', type: '2' }]);
|
|
55
|
+
expect(await refused(s, () => s.install())).toBeInstanceOf(ArchiveRefused);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('the declared member missing from a correctly pinned archive', async () => {
|
|
59
|
+
const s = setup([{ bytes: BINARY.vmagent, name: 'vmagent-prod' }]);
|
|
60
|
+
const error = await refused(s, () => s.install());
|
|
61
|
+
expect(error.message).toContain(
|
|
62
|
+
'no member named exactly "vmalert-prod" (it has "vmagent-prod")',
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('a failed download', async () => {
|
|
67
|
+
const s = setup();
|
|
68
|
+
const error = await refused(s, () =>
|
|
69
|
+
reconcileBinary(s.fake.runner, fakeTransport({}).fetch, s.props()),
|
|
70
|
+
);
|
|
71
|
+
expect(error).toBeInstanceOf(DownloadFailed);
|
|
72
|
+
expect(error.message).toContain(`Release.Binary ${PATH}:`);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const archive = (more: object) => (p: ReleaseBinaryProps) => ({
|
|
76
|
+
archive: { ...p.archive, ...more },
|
|
77
|
+
});
|
|
78
|
+
test.each([
|
|
79
|
+
['an archive digest that is not hex', archive({ sha256: 'abc' })],
|
|
80
|
+
['an asset given as a glob', archive({ asset: 'vmutils-darwin-arm64-*.tar.gz' })],
|
|
81
|
+
['an asset that is not a tarball', archive({ asset: 'vmutils-darwin-arm64-v1.151.0.zip' })],
|
|
82
|
+
['a repo that climbs', archive({ repo: '../VictoriaMetrics' })],
|
|
83
|
+
['a tag with a slash', archive({ tag: 'v1.151.0/../x' })],
|
|
84
|
+
['no size', archive({ size: 0 })],
|
|
85
|
+
['a size past the one-gibibyte ceiling', archive({ size: 2 ** 30 + 1 })],
|
|
86
|
+
['a repo named ..', archive({ repo: 'VictoriaMetrics/..' })],
|
|
87
|
+
['an upper-case member digest', () => ({ sha256: 'A'.repeat(64) })],
|
|
88
|
+
['an absolute member', () => ({ member: '/vmalert-prod' })],
|
|
89
|
+
['a member that climbs', () => ({ member: '../vmalert-prod' })],
|
|
90
|
+
['a name with a slash', () => ({ name: 'bin/vmalert' })],
|
|
91
|
+
['a relative directory', () => ({ directory: 'bin/vmutils-1.151.0' })],
|
|
92
|
+
['a setuid mode', () => ({ mode: 0o4755 })],
|
|
93
|
+
['a group-writable mode', () => ({ mode: 0o775 })],
|
|
94
|
+
// Measured 2026-09-22 before the fix: written, then EACCES on the read-back, left with no state.
|
|
95
|
+
['an execute-only mode its owner cannot re-hash', () => ({ mode: 0o111 })],
|
|
96
|
+
])('%s: refused before any host call or download', async (_, change) => {
|
|
97
|
+
const s = setup();
|
|
98
|
+
s.fake.calls.length = 0;
|
|
99
|
+
const base = s.props();
|
|
100
|
+
const error = await refused(s, () =>
|
|
101
|
+
s.install({ ...base, ...change(base) } as ReleaseBinaryProps),
|
|
102
|
+
);
|
|
103
|
+
expect(error).toBeInstanceOf(BinaryRefused);
|
|
104
|
+
expect(s.transport.requests).toEqual([]);
|
|
105
|
+
expect(s.fake.calls).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('a new pin at the same path: refused, the installed binary untouched', async () => {
|
|
109
|
+
const s = setup();
|
|
110
|
+
const olds = s.props();
|
|
111
|
+
const output = await s.install(olds);
|
|
112
|
+
s.transport.requests.length = 0;
|
|
113
|
+
s.fake.calls.length = 0;
|
|
114
|
+
const bumped = s.props({
|
|
115
|
+
archive: { ...olds.archive, tag: 'v1.152.0' },
|
|
116
|
+
sha256: 'e'.repeat(64),
|
|
117
|
+
});
|
|
118
|
+
const error = await refused(s, () => s.install(bumped, { olds, output }));
|
|
119
|
+
expect(error.message).toContain('would overwrite it in place');
|
|
120
|
+
expect(s.transport.requests).toEqual([]);
|
|
121
|
+
expect(s.fake.files.get(PATH)?.bytes).toEqual(BINARY.vmalert);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test('a missing directory: refused before the download', async () => {
|
|
125
|
+
const s = setup();
|
|
126
|
+
s.fake.dirs.delete(VMUTILS_DIR);
|
|
127
|
+
const error = await refused(s, () => s.install());
|
|
128
|
+
expect(error.message).toContain(`directory ${VMUTILS_DIR} does not exist`);
|
|
129
|
+
expect(s.transport.requests).toEqual([]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('⛔ a directory others may write: refused before the download', async () => {
|
|
133
|
+
const s = setup();
|
|
134
|
+
const stat = s.fake.runner.stat;
|
|
135
|
+
s.fake.runner.stat = async (path) => {
|
|
136
|
+
const found = await stat(path);
|
|
137
|
+
return path === VMUTILS_DIR && found !== undefined ? { ...found, mode: 0o777 } : found;
|
|
138
|
+
};
|
|
139
|
+
const error = await refused(s, () => s.install());
|
|
140
|
+
expect(error.message).toContain('is writable by group or other (mode 0777)');
|
|
141
|
+
expect(s.transport.requests).toEqual([]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('a file this resource does not own, without --adopt: refused before the download', async () => {
|
|
145
|
+
const s = setup();
|
|
146
|
+
s.fake.files.set(PATH, { bytes: bytesOf('theirs'), gid: 0, kind: 'file', mode: 0o755, uid: 0 });
|
|
147
|
+
const error = await refused(s, () => s.install());
|
|
148
|
+
expect(error.message).toContain('already exists and is not this resource');
|
|
149
|
+
expect(s.transport.requests).toEqual([]);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ⛔ binary-claim.ts: with no state, only the pinned binary, and only under --adopt.
|
|
153
|
+
test('the pinned bytes at the path with no state, without --adopt: refused, not claimed', async () => {
|
|
154
|
+
const s = setup();
|
|
155
|
+
s.fake.files.set(PATH, { bytes: BINARY.vmalert, gid: 0, kind: 'file', mode: 0o755, uid: 0 });
|
|
156
|
+
const error = await refused(s, () => s.install());
|
|
157
|
+
expect(error.message).toContain('it holds the pinned binary, but this stack holds no state');
|
|
158
|
+
expect(s.transport.requests).toEqual([]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('other bytes at the path under --adopt: refused before the download, their file kept', async () => {
|
|
162
|
+
const s = setup();
|
|
163
|
+
const theirs = bytesOf('theirs');
|
|
164
|
+
s.fake.files.set(PATH, { bytes: theirs, gid: 0, kind: 'file', mode: 0o755, uid: 0 });
|
|
165
|
+
const error = await refused(s, () => s.install(s.props(), { adopt: true }));
|
|
166
|
+
expect(error.message).toContain(`a file hashing to ${sha256Hex(theirs)} is there`);
|
|
167
|
+
expect(s.transport.requests).toEqual([]);
|
|
168
|
+
expect(s.fake.files.get(PATH)?.bytes).toEqual(theirs);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test('a write that does not read back as declared: the create is rolled back', async () => {
|
|
172
|
+
const s = setup();
|
|
173
|
+
const write = s.fake.runner.writeFileAtomic;
|
|
174
|
+
s.fake.runner.writeFileAtomic = (path, bytes, options) =>
|
|
175
|
+
write(path, bytes, { ...options, mode: 0o700 });
|
|
176
|
+
const before = pathsOn(s.fake);
|
|
177
|
+
await expect(s.install()).rejects.toThrow('does not match the declaration');
|
|
178
|
+
expect(pathsOn(s.fake)).toEqual(before);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test('⛔ a read-back that THROWS rolls the create back too, as a refusal', async () => {
|
|
182
|
+
const s = setup();
|
|
183
|
+
const read = s.fake.runner.readFile;
|
|
184
|
+
s.fake.runner.readFile = async (path) => {
|
|
185
|
+
if (s.fake.files.has(path)) throw Object.assign(new Error('EACCES'), { code: 'EACCES' });
|
|
186
|
+
return read(path);
|
|
187
|
+
};
|
|
188
|
+
const before = pathsOn(s.fake);
|
|
189
|
+
await expect(s.install()).rejects.toThrow(`Release.Binary ${PATH}: the write returned but`);
|
|
190
|
+
expect(pathsOn(s.fake)).toEqual(before);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The install through the kit's REAL sudoRunner over its fake host (launchd/fake-sudo.ts), so what
|
|
3
|
+
* is asserted is the runner's own staging, not a fake's: the verified bytes reach root's directory
|
|
4
|
+
* as one `install -S` of one staged file, and no failure — before the write or during it — leaves a
|
|
5
|
+
* staged file behind.
|
|
6
|
+
*
|
|
7
|
+
* ★ WHY THIS IS THE STAGING TEST. Release.Binary stages nothing itself: the archive and the member
|
|
8
|
+
* live in memory (archive.ts). The only staged file in an install is the runner's — local-runner's
|
|
9
|
+
* same-directory temp file (local-runner.test.ts: "leaves no temp file behind") or, here,
|
|
10
|
+
* sudoRunner's 0600 file in a 0700 directory.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, test } from 'bun:test';
|
|
13
|
+
import { fakeSudoHost } from '../launchd/fake-sudo.ts';
|
|
14
|
+
import { INSTALL } from '../launchd/sudo-allowlist.ts';
|
|
15
|
+
import type { ReleaseBinaryProps } from './binary-form.ts';
|
|
16
|
+
import { reconcileBinary } from './binary-lifecycle.ts';
|
|
17
|
+
import {
|
|
18
|
+
BINARY,
|
|
19
|
+
type TarEntry,
|
|
20
|
+
VMUTILS_ENTRIES,
|
|
21
|
+
VMUTILS_URL,
|
|
22
|
+
bytesOf,
|
|
23
|
+
fakeTransport,
|
|
24
|
+
gzip,
|
|
25
|
+
syntheticRelease,
|
|
26
|
+
tarOf,
|
|
27
|
+
vmalertProps,
|
|
28
|
+
} from './fake-release.ts';
|
|
29
|
+
|
|
30
|
+
const DIR = '/opt/example/app/vmutils-1.151.0';
|
|
31
|
+
const PATH = `${DIR}/vmalert`;
|
|
32
|
+
|
|
33
|
+
type Setup = {
|
|
34
|
+
/** What the transport serves at the vmutils URL; undefined serves nothing (a 404). */
|
|
35
|
+
readonly served?: (release: ReturnType<typeof syntheticRelease>) => Uint8Array | undefined;
|
|
36
|
+
readonly entries?: readonly TarEntry[];
|
|
37
|
+
readonly declare?: Partial<ReleaseBinaryProps>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const setup = ({ declare = {}, entries, served = (r) => r.vmutils }: Setup = {}) => {
|
|
41
|
+
const release = syntheticRelease(entries);
|
|
42
|
+
const host = fakeSudoHost();
|
|
43
|
+
host.fake.dirs.set(DIR, 0);
|
|
44
|
+
const bytes = served(release);
|
|
45
|
+
const transport = fakeTransport(bytes === undefined ? {} : { [VMUTILS_URL]: bytes });
|
|
46
|
+
const props = vmalertProps(release.catalog, { directory: DIR, group: 0, owner: 0, ...declare });
|
|
47
|
+
const install = () => reconcileBinary(host.runner, transport.fetch, props);
|
|
48
|
+
return { host, install, transport };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
describe('Release.Binary through sudoRunner', () => {
|
|
52
|
+
test('one staged file, one exact install, and the staged file removed', async () => {
|
|
53
|
+
const { host, install } = setup();
|
|
54
|
+
const attrs = await install();
|
|
55
|
+
expect(host.privileged()).toEqual([
|
|
56
|
+
[INSTALL, '-S', '-m', '0755', '-o', '0', '-g', '0', expect.any(String), PATH],
|
|
57
|
+
]);
|
|
58
|
+
expect(host.fake.files.get(PATH)?.bytes).toEqual(BINARY.vmalert);
|
|
59
|
+
expect(attrs).toMatchObject({ gid: 0, mode: 0o755, uid: 0 });
|
|
60
|
+
expect(host.state.stagedCount).toBe(1);
|
|
61
|
+
expect(host.staged.size).toBe(0);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
test('a failed install throws and leaves no staged file and no binary', async () => {
|
|
65
|
+
const { host, install } = setup();
|
|
66
|
+
host.state.installFailure = { exitCode: 71, stderr: 'install: disk full', stdout: '' };
|
|
67
|
+
await expect(install()).rejects.toThrow('disk full');
|
|
68
|
+
expect(host.state.stagedCount).toBe(1);
|
|
69
|
+
expect(host.staged.size).toBe(0);
|
|
70
|
+
expect(host.fake.files.has(PATH)).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// ⛔ EVERY failure before the write: nothing is staged at all, sudo is never asked, no binary.
|
|
74
|
+
const tampered = () =>
|
|
75
|
+
gzip(tarOf(VMUTILS_ENTRIES.map((e) => ({ ...e, bytes: bytesOf('evil') }))));
|
|
76
|
+
test.each<[string, Setup]>([
|
|
77
|
+
['a wrong archive hash', { served: tampered }],
|
|
78
|
+
['a wrong binary hash', { declare: { sha256: 'f'.repeat(64) } }],
|
|
79
|
+
['a tar-slip entry', { entries: [...VMUTILS_ENTRIES, { bytes: bytesOf('x'), name: '../x' }] }],
|
|
80
|
+
['the member as a symlink', { entries: [{ name: 'vmalert-prod', type: '2' }] }],
|
|
81
|
+
['the member missing', { entries: [{ bytes: BINARY.vmagent, name: 'vmagent-prod' }] }],
|
|
82
|
+
['a failed download', { served: () => undefined }],
|
|
83
|
+
['a malformed pin', { declare: { sha256: 'not-a-digest' } }],
|
|
84
|
+
['an unwritable mode', { declare: { mode: 0o777 } }],
|
|
85
|
+
])('%s stages nothing, asks sudo nothing, writes nothing', async (_, how) => {
|
|
86
|
+
const { host, install } = setup(how);
|
|
87
|
+
await expect(install()).rejects.toThrow();
|
|
88
|
+
expect(host.state.stagedCount).toBe(0);
|
|
89
|
+
expect(host.staged.size).toBe(0);
|
|
90
|
+
expect(host.sudoCalls).toEqual([]);
|
|
91
|
+
expect(host.fake.files.has(PATH)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Release.Binary` — one binary out of a pinned release archive, written onto a host and verified
|
|
3
|
+
* twice: the archive against its pinned SHA-256, then the binary against its own.
|
|
4
|
+
*
|
|
5
|
+
* - reconcile — validate every prop; download the archive by its exact URL; verify it; unpack ONLY
|
|
6
|
+
* the declared member; verify that; write it atomically through the HostRunner; read it back. A
|
|
7
|
+
* file already holding the pinned bytes is never downloaded again.
|
|
8
|
+
* - read — lstat plus the SHA-256 of the bytes; never runs the binary.
|
|
9
|
+
* - diff — the pins, the path, then mode and owner against the live file (binary-diff.ts).
|
|
10
|
+
* - replace — when the path moves (a new version is a new directory): create-before-delete.
|
|
11
|
+
* - delete — removes the file, never the directory.
|
|
12
|
+
*
|
|
13
|
+
* ★ VENDOR-NEUTRAL. The pinned archive arrives as props, usually from a data set beside this
|
|
14
|
+
* (victoria.ts) through `catalogBinary()`; nothing here knows a vendor, a package or a version.
|
|
15
|
+
* ⛔ IT INSTALLS; IT NEVER STARTS. No launchctl, no systemctl, no restart: the job that runs the
|
|
16
|
+
* binary is the stack's own `LaunchdJob`, which puts this resource's `path` in its argv so the
|
|
17
|
+
* engine installs first and restarts the job when the path moves.
|
|
18
|
+
* ⚠️ NOTHING IS ADOPTED WITHOUT `--adopt`, AT PLAN OR AT APPLY: the probe reads a file already at
|
|
19
|
+
* the path as `Unowned` — even one holding exactly the pinned bytes (docs/ownership.md) — and
|
|
20
|
+
* where the probe never ran (a prop was an Output) reconcile refuses it the same way. With
|
|
21
|
+
* `--adopt`, a file holding the pinned bytes is recognised by its digest and taken over without
|
|
22
|
+
* a download. ⛔ A file with OTHER bytes is never adopted, `--adopt` or not: that would be an
|
|
23
|
+
* overwrite in place that the plan prints as `adopted` (binary-claim.ts, both halves).
|
|
24
|
+
*/
|
|
25
|
+
import { Resource } from 'alchemy';
|
|
26
|
+
import * as Provider from 'alchemy/Provider';
|
|
27
|
+
import * as Effect from 'effect/Effect';
|
|
28
|
+
import * as HttpClient from 'effect/unstable/http/HttpClient';
|
|
29
|
+
import { lift } from '../launchd/host-effect.ts';
|
|
30
|
+
import { HostRunnerService } from '../launchd/runner.ts';
|
|
31
|
+
import { adoptsAtApply } from '../ownership/adopt.ts';
|
|
32
|
+
import { noteUnfinished } from '../ownership/resume.ts';
|
|
33
|
+
import { diffBinary } from './binary-diff.ts';
|
|
34
|
+
import {
|
|
35
|
+
type ReleaseBinaryAttributes,
|
|
36
|
+
type ReleaseBinaryProps,
|
|
37
|
+
releaseBinaryPath,
|
|
38
|
+
} from './binary-form.ts';
|
|
39
|
+
import {
|
|
40
|
+
NOTHING,
|
|
41
|
+
deleteBinary,
|
|
42
|
+
reconcileBinary,
|
|
43
|
+
refreshBinary,
|
|
44
|
+
refuse,
|
|
45
|
+
} from './binary-lifecycle.ts';
|
|
46
|
+
import { readWithoutState } from './binary-read.ts';
|
|
47
|
+
import { declaredPinProblems } from './declared-pins.ts';
|
|
48
|
+
import { type DownloadPolicy, httpFetchArchive, sharingInFlight } from './download.ts';
|
|
49
|
+
|
|
50
|
+
export type { ReleaseBinaryAttributes, ReleaseBinaryProps } from './binary-form.ts';
|
|
51
|
+
|
|
52
|
+
export interface ReleaseBinary extends Resource<
|
|
53
|
+
'Release.Binary',
|
|
54
|
+
ReleaseBinaryProps,
|
|
55
|
+
ReleaseBinaryAttributes
|
|
56
|
+
> {}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* One binary from a pinned release archive, installed onto a host and verified against digests
|
|
60
|
+
* the stack pins in code — the archive's, then the binary's own.
|
|
61
|
+
*
|
|
62
|
+
* ### Installing a binary
|
|
63
|
+
*
|
|
64
|
+
* The directory is declared first and passed by its `path`, which orders the two; the job that
|
|
65
|
+
* runs the binary references the binary's `path`, which orders the job after it.
|
|
66
|
+
*
|
|
67
|
+
* **Example:**
|
|
68
|
+
*
|
|
69
|
+
* ```ts
|
|
70
|
+
* const vmalertOf = { package: 'vmutils', version: '1.151.0', platform: 'darwin-arm64', binary: 'vmalert' };
|
|
71
|
+
* const dir = yield* HostDirectory('vmutils-1.151.0', {
|
|
72
|
+
* path: catalogDirectory('/opt/example/bin', vmalertOf),
|
|
73
|
+
* mode: 0o755,
|
|
74
|
+
* });
|
|
75
|
+
* const vmalert = yield* ReleaseBinary('vmalert', {
|
|
76
|
+
* ...catalogBinary(VICTORIA_RELEASES, vmalertOf),
|
|
77
|
+
* directory: dir.path,
|
|
78
|
+
* });
|
|
79
|
+
* // LaunchdJob(…, { programArguments: [vmalert.path, '--httpListenAddr=127.0.0.1:8880'] })
|
|
80
|
+
* ```
|
|
81
|
+
*
|
|
82
|
+
* @resource
|
|
83
|
+
*/
|
|
84
|
+
export const ReleaseBinary = Resource<ReleaseBinary>('Release.Binary');
|
|
85
|
+
|
|
86
|
+
/** Test seams — NOT on the barrel. Every caller outside tests uses the defaults. */
|
|
87
|
+
export type ReleaseBinaryInternals = { readonly download?: DownloadPolicy };
|
|
88
|
+
|
|
89
|
+
export const makeReleaseBinaryProvider = (internals: ReleaseBinaryInternals = {}) =>
|
|
90
|
+
Provider.effect(
|
|
91
|
+
ReleaseBinary,
|
|
92
|
+
Effect.gen(function* () {
|
|
93
|
+
const runner = yield* HostRunnerService;
|
|
94
|
+
const client = yield* HttpClient.HttpClient;
|
|
95
|
+
const fetch = sharingInFlight(httpFetchArchive(client, internals.download));
|
|
96
|
+
/**
|
|
97
|
+
* ⛔ ONE PATH, ONE DECLARATION. MEASURED 2026-09-22 (binary-claims.test.ts, before this): two
|
|
98
|
+
* resources installing one path in one deploy both planned `create`, shared one download,
|
|
99
|
+
* and both recorded the file as theirs. Dropping either later — gating vmalert-logs off
|
|
100
|
+
* along with its own ReleaseBinary, say — deleted the file the other still declared, and
|
|
101
|
+
* that deploy reported the survivor `noop`. So a second resource claiming a path in the
|
|
102
|
+
* same run is refused before it touches anything. Two jobs that run one binary share ONE
|
|
103
|
+
* ReleaseBinary. ⚠️ Keyed by spelling: one file under two spellings is not caught here.
|
|
104
|
+
*/
|
|
105
|
+
const claims = new Map<string, string>();
|
|
106
|
+
const claim = (fqn: string, props: ReleaseBinaryProps): void => {
|
|
107
|
+
const path = releaseBinaryPath(props);
|
|
108
|
+
const holder = claims.get(path);
|
|
109
|
+
if (holder !== undefined && holder !== fqn) {
|
|
110
|
+
throw refuse(
|
|
111
|
+
path,
|
|
112
|
+
`is already installed by ${holder} in this deploy; declare each binary path once, ` +
|
|
113
|
+
`and let every job that runs it share that one resource. ${NOTHING}`,
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
claims.set(path, fqn);
|
|
117
|
+
};
|
|
118
|
+
return ReleaseBinary.Provider.of({
|
|
119
|
+
list: () => Effect.succeed([]),
|
|
120
|
+
|
|
121
|
+
// ⛔ The probe (no `output`) validates every prop first, so a malformed pin fails the plan
|
|
122
|
+
// here — before any host call, let alone a download. A recovery read never fails its
|
|
123
|
+
// caller (binary-read.ts): its props are the failed deploy's.
|
|
124
|
+
read: ({ fqn, instanceId, olds, output }) =>
|
|
125
|
+
output === undefined
|
|
126
|
+
? readWithoutState(runner, { fqn, instanceId, olds })
|
|
127
|
+
: lift(() => refreshBinary(runner, output)),
|
|
128
|
+
|
|
129
|
+
diff: ({ instanceId, news, olds, output }) =>
|
|
130
|
+
output === undefined
|
|
131
|
+
? noteUnfinished(instanceId)
|
|
132
|
+
: lift(() => diffBinary(runner, news, olds, output)),
|
|
133
|
+
|
|
134
|
+
// ★ `--adopt` reaches the apply too: the probe never ran for a create with an Output prop.
|
|
135
|
+
// ⛔ So do the pins AS DECLARED: `news` is resolved by now, and a first deploy is never
|
|
136
|
+
// diffed, so only the stack's own record can show a digest that was an Output.
|
|
137
|
+
reconcile: ({ fqn, instanceId, news, olds, output, session }) =>
|
|
138
|
+
Effect.gen(function* () {
|
|
139
|
+
const declared = yield* declaredPinProblems(fqn);
|
|
140
|
+
const adopt = yield* adoptsAtApply({ fqn, instanceId, output });
|
|
141
|
+
return yield* lift(async () => {
|
|
142
|
+
claim(fqn, news);
|
|
143
|
+
return reconcileBinary(runner, fetch, news, {
|
|
144
|
+
adopt,
|
|
145
|
+
declared,
|
|
146
|
+
note: (message) => Effect.runPromise(session.note(message)),
|
|
147
|
+
olds,
|
|
148
|
+
output,
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}),
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* ⛔ NEVER REMOVE A FILE ANOTHER DECLARATION INSTALLED IN THIS DEPLOY. Alchemy runs every
|
|
155
|
+
* delete (Apply.ts collectGarbage, Phase 2) only after every reconcile has succeeded, so a
|
|
156
|
+
* claim by another name means the file is that resource's now, verified as its pin.
|
|
157
|
+
* 🔴 MEASURED 2026-09-22 (adopt-parity.test.ts): a rename under --adopt — the new name
|
|
158
|
+
* claimed the file, the old name's orphan delete removed it, and the deploy succeeded
|
|
159
|
+
* with the binary gone. ★ Left in place rather than removed: a stray file is a tidy-up, a
|
|
160
|
+
* missing binary is an outage. ⚠️ Keyed by spelling, like the claims themselves.
|
|
161
|
+
* ★ A deleted resource gives its claim back, so a provider that outlives one deploy never
|
|
162
|
+
* refuses the next resource declared at that path.
|
|
163
|
+
*/
|
|
164
|
+
delete: ({ fqn, output, session }) =>
|
|
165
|
+
lift(async () => {
|
|
166
|
+
const holder = claims.get(output.path);
|
|
167
|
+
if (holder !== undefined && holder !== fqn) {
|
|
168
|
+
await Effect.runPromise(
|
|
169
|
+
session.note(`${output.path} was installed by ${holder} in this deploy; kept`),
|
|
170
|
+
);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
await deleteBinary(runner, output);
|
|
174
|
+
if (holder === fqn) claims.delete(output.path);
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
}),
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
/** The provider, as a stack uses it. It needs a HostRunnerService and an HttpClient. */
|
|
181
|
+
export const ReleaseBinaryProvider = () => makeReleaseBinaryProvider();
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A catalog request → pinned props, or a refusal — pure, over the REAL Victoria data set. Every
|
|
3
|
+
* refusal here happens in the stack's own program, so it fails the plan before any resource runs
|
|
4
|
+
* (plan.test.ts proves that through the engine).
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from 'bun:test';
|
|
7
|
+
import { binaryProblems } from './binary-form.ts';
|
|
8
|
+
import { catalogBinary, catalogDirectory, catalogProblems, identifyBinary } from './catalog.ts';
|
|
9
|
+
import { BinaryRefused } from './refused.ts';
|
|
10
|
+
import { VICTORIA_RELEASES } from './victoria.ts';
|
|
11
|
+
|
|
12
|
+
const VMALERT = {
|
|
13
|
+
binary: 'vmalert',
|
|
14
|
+
package: 'vmutils',
|
|
15
|
+
platform: 'darwin-arm64',
|
|
16
|
+
version: '1.151.0',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('refusing what the catalog does not pin', () => {
|
|
20
|
+
test.each([
|
|
21
|
+
[
|
|
22
|
+
'a version the catalog does not pin',
|
|
23
|
+
{ version: '1.152.0' },
|
|
24
|
+
'vmutils 1.152.0 is not in the catalog',
|
|
25
|
+
],
|
|
26
|
+
['the tag spelling', { version: 'v1.151.0' }, 'is not in the catalog'],
|
|
27
|
+
['an enterprise version', { version: '1.151.0-enterprise' }, 'is not in the catalog'],
|
|
28
|
+
[
|
|
29
|
+
'a cluster package',
|
|
30
|
+
{ package: 'victoria-metrics-cluster' },
|
|
31
|
+
'package "victoria-metrics-cluster"',
|
|
32
|
+
],
|
|
33
|
+
['the vendor member name', { binary: 'vmalert-prod' }, 'that is the archive member'],
|
|
34
|
+
['a binary vmutils does not ship', { binary: 'vminsert' }, 'has no binary "vminsert"'],
|
|
35
|
+
['another platform', { platform: 'linux-amd64' }, 'not pinned for platform "linux-amd64"'],
|
|
36
|
+
['an inherited key', { package: 'constructor' }, 'package "constructor"'],
|
|
37
|
+
])('refuses %s', (_, change, message) => {
|
|
38
|
+
const request = { ...VMALERT, ...change };
|
|
39
|
+
expect(catalogProblems(VICTORIA_RELEASES, request).join('; ')).toContain(message);
|
|
40
|
+
expect(() => catalogBinary(VICTORIA_RELEASES, request)).toThrow(message);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('the refusal is typed, names the binary, and says nothing was fetched', () => {
|
|
44
|
+
let caught: unknown;
|
|
45
|
+
try {
|
|
46
|
+
catalogBinary(VICTORIA_RELEASES, { ...VMALERT, version: '1.152.0' });
|
|
47
|
+
} catch (cause) {
|
|
48
|
+
caught = cause;
|
|
49
|
+
}
|
|
50
|
+
expect(caught).toBeInstanceOf(BinaryRefused);
|
|
51
|
+
expect((caught as Error).message).toMatch(
|
|
52
|
+
/^Release\.Binary vmalert: .* Nothing was fetched\.$/,
|
|
53
|
+
);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('what a pinned request becomes', () => {
|
|
58
|
+
test('props that validate once the stack adds a directory', () => {
|
|
59
|
+
const pinned = catalogBinary(VICTORIA_RELEASES, VMALERT);
|
|
60
|
+
const directory = catalogDirectory('/opt/example/bin', VMALERT);
|
|
61
|
+
expect(directory).toBe('/opt/example/bin/vmutils-1.151.0');
|
|
62
|
+
expect(binaryProblems({ ...pinned, directory })).toEqual([]);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
test('every binary of every pinned archive resolves to valid props', () => {
|
|
66
|
+
for (const [pkg, entry] of Object.entries(VICTORIA_RELEASES.packages)) {
|
|
67
|
+
for (const [version, platforms] of Object.entries(entry.versions)) {
|
|
68
|
+
for (const platform of Object.keys(platforms)) {
|
|
69
|
+
for (const binary of Object.keys(entry.binaries)) {
|
|
70
|
+
const request = { binary, package: pkg, platform, version };
|
|
71
|
+
const props = { ...catalogBinary(VICTORIA_RELEASES, request), directory: '/x/y' };
|
|
72
|
+
expect(binaryProblems(props)).toEqual([]);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('identifying an installed file', () => {
|
|
81
|
+
test('by its digest alone', () => {
|
|
82
|
+
const pin = '4759ec89a466129b9eefc22244a111e1f489739340747805fd93b58fd0f7b2dd';
|
|
83
|
+
expect(identifyBinary([VICTORIA_RELEASES], pin)).toEqual({
|
|
84
|
+
binary: 'victoria-traces',
|
|
85
|
+
package: 'victoria-traces',
|
|
86
|
+
platform: 'darwin-arm64',
|
|
87
|
+
vendor: 'VictoriaMetrics',
|
|
88
|
+
version: '0.10.0',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("an archive's digest is not a binary's: identity is by the member's own line", () => {
|
|
93
|
+
const archive = 'cbe83f1d409cbb85fbf1c890c4a9b7fd34c1d74acec4dce2e0ebe46fcb260e38';
|
|
94
|
+
expect(identifyBinary([VICTORIA_RELEASES], archive)).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
});
|