@happier-dev/stack 0.1.0-preview.100.1 → 0.1.0-preview.134.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/docs/server-flavors.md +6 -6
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.d.ts.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/index.js +2 -0
- package/node_modules/@happier-dev/cli-common/dist/index.js.map +1 -1
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts +51 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js +129 -0
- package/node_modules/@happier-dev/cli-common/dist/providers/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js +5 -0
- package/node_modules/@happier-dev/cli-common/dist/service/index.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts +15 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js +97 -0
- package/node_modules/@happier-dev/cli-common/dist/service/launchd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts +55 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js +302 -0
- package/node_modules/@happier-dev/cli-common/dist/service/manager.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts +12 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js +75 -0
- package/node_modules/@happier-dev/cli-common/dist/service/systemd.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts +8 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.d.ts.map +1 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js +29 -0
- package/node_modules/@happier-dev/cli-common/dist/service/windows.js.map +1 -0
- package/node_modules/@happier-dev/cli-common/package.json +11 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts +22 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js +44 -0
- package/node_modules/@happier-dev/release-runtime/dist/assets.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts +5 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js +21 -0
- package/node_modules/@happier-dev/release-runtime/dist/checksums.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts +14 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js +39 -0
- package/node_modules/@happier-dev/release-runtime/dist/extractPlan.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts +20 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js +60 -0
- package/node_modules/@happier-dev/release-runtime/dist/github.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/index.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts +7 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js +92 -0
- package/node_modules/@happier-dev/release-runtime/dist/minisign.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts +26 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.d.ts.map +1 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js +53 -0
- package/node_modules/@happier-dev/release-runtime/dist/verifiedDownload.js.map +1 -0
- package/node_modules/@happier-dev/release-runtime/package.json +38 -0
- package/package.json +4 -2
- package/scripts/auth.mjs +3 -2
- package/scripts/auth_copy_from_pglite_lock_in_use.integration.test.mjs +1 -0
- package/scripts/auth_copy_from_runCapture.integration.test.mjs +8 -1
- package/scripts/build.mjs +3 -18
- package/scripts/bundleWorkspaceDeps.mjs +5 -1
- package/scripts/bundleWorkspaceDeps.test.mjs +16 -0
- package/scripts/mobile.mjs +32 -1
- package/scripts/mobile_prebuild_happyDir_defined.test.mjs +47 -0
- package/scripts/mobile_prebuild_sets_rct_metro_port.test.mjs +81 -0
- package/scripts/mobile_run_ios_passes_port.integration.test.mjs +101 -0
- package/scripts/providers_cmd.mjs +262 -0
- package/scripts/release_binary_smoke.integration.test.mjs +25 -6
- package/scripts/remote_cmd.mjs +240 -0
- package/scripts/self_host_daemon.real.integration.test.mjs +296 -0
- package/scripts/self_host_launchd.real.integration.test.mjs +211 -0
- package/scripts/self_host_runtime.mjs +1403 -312
- package/scripts/self_host_runtime.test.mjs +361 -1
- package/scripts/self_host_schtasks.real.integration.test.mjs +217 -0
- package/scripts/self_host_service_e2e_harness.mjs +93 -0
- package/scripts/self_host_systemd.real.integration.test.mjs +8 -86
- package/scripts/service.mjs +156 -26
- package/scripts/stack/command_arguments.mjs +1 -0
- package/scripts/stack/help_text.mjs +2 -0
- package/scripts/stack_daemon_cmd.integration.test.mjs +37 -0
- package/scripts/stack_happy_cmd.integration.test.mjs +36 -0
- package/scripts/utils/auth/credentials_paths.mjs +9 -9
- package/scripts/utils/auth/credentials_paths.test.mjs +8 -0
- package/scripts/utils/auth/stable_scope_id.mjs +1 -1
- package/scripts/utils/cli/cli_registry.mjs +18 -0
- package/scripts/utils/cli/progress.mjs +8 -1
- package/scripts/utils/cli/progress.test.mjs +43 -0
- package/scripts/utils/dev/expo_dev.buildEnv.test.mjs +17 -0
- package/scripts/utils/dev/expo_dev.mjs +35 -5
- package/scripts/utils/dev/expo_dev_restart_port_reservation.test.mjs +180 -1
- package/scripts/utils/dev/expo_dev_runtime_metadata.test.mjs +126 -0
- package/scripts/utils/dev/expo_dev_verbose_logs.test.mjs +9 -2
- package/scripts/utils/server/port.mjs +20 -2
- package/scripts/utils/service/service_manager.definition.test.mjs +66 -0
- package/scripts/utils/service/service_manager.mjs +96 -0
- package/scripts/utils/service/service_manager.plan.test.mjs +37 -0
- package/scripts/utils/service/service_manager.test.mjs +20 -0
- package/scripts/utils/service/systemd_service_unit.mjs +1 -0
- package/scripts/utils/service/systemd_service_unit.test.mjs +42 -0
- package/scripts/utils/service/windows_schtasks_wrapper.mjs +1 -0
- package/scripts/utils/service/windows_schtasks_wrapper.test.mjs +25 -0
- package/scripts/utils/ui/ui_export_env.mjs +29 -0
- package/scripts/utils/ui/ui_export_env.test.mjs +25 -0
- package/scripts/worktrees.mjs +3 -0
- package/scripts/worktrees_status_default_target.test.mjs +56 -0
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises';
|
|
2
3
|
import test from 'node:test';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
3
6
|
|
|
4
7
|
import {
|
|
5
8
|
parseSelfHostInvocation,
|
|
6
9
|
pickReleaseAsset,
|
|
10
|
+
resolveMinisignPublicKeyText,
|
|
11
|
+
resolveSelfHostAutoUpdateDefault,
|
|
12
|
+
resolveSelfHostAutoUpdateIntervalMinutes,
|
|
13
|
+
resolveSelfHostHealthTimeoutMs,
|
|
14
|
+
resolveSelfHostDefaults,
|
|
15
|
+
renderUpdaterLaunchdPlistXml,
|
|
16
|
+
renderUpdaterScheduledTaskWrapperPs1,
|
|
17
|
+
renderUpdaterSystemdUnit,
|
|
18
|
+
renderUpdaterSystemdTimerUnit,
|
|
7
19
|
renderServerEnvFile,
|
|
8
20
|
renderServerServiceUnit,
|
|
21
|
+
renderSelfHostStatusText,
|
|
22
|
+
buildSelfHostDoctorChecks,
|
|
23
|
+
normalizeSelfHostAutoUpdateState,
|
|
24
|
+
decideSelfHostAutoUpdateReconcile,
|
|
25
|
+
mergeEnvTextWithDefaults,
|
|
9
26
|
} from './self_host_runtime.mjs';
|
|
10
27
|
|
|
11
28
|
test('parseSelfHostInvocation accepts optional self-host prefix', () => {
|
|
@@ -48,7 +65,37 @@ test('pickReleaseAsset rejects releases missing minisign signature assets', () =
|
|
|
48
65
|
os: 'linux',
|
|
49
66
|
arch: 'x64',
|
|
50
67
|
});
|
|
51
|
-
}, /signature/i);
|
|
68
|
+
}, /minisig|signature/i);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('pickReleaseAsset supports windows zip artifacts', () => {
|
|
72
|
+
const assets = [
|
|
73
|
+
{ name: 'happier-server-v1.2.3-windows-x64.zip', browser_download_url: 'https://example.test/server.zip' },
|
|
74
|
+
{ name: 'checksums-happier-server-v1.2.3.txt', browser_download_url: 'https://example.test/checksums.txt' },
|
|
75
|
+
{ name: 'checksums-happier-server-v1.2.3.txt.minisig', browser_download_url: 'https://example.test/checksums.txt.minisig' },
|
|
76
|
+
];
|
|
77
|
+
const picked = pickReleaseAsset({
|
|
78
|
+
assets,
|
|
79
|
+
product: 'happier-server',
|
|
80
|
+
os: 'windows',
|
|
81
|
+
arch: 'x64',
|
|
82
|
+
});
|
|
83
|
+
assert.equal(picked.archiveUrl, 'https://example.test/server.zip');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('pickReleaseAsset supports windows tar.gz artifacts', () => {
|
|
87
|
+
const assets = [
|
|
88
|
+
{ name: 'happier-server-v1.2.3-windows-x64.tar.gz', browser_download_url: 'https://example.test/server.tgz' },
|
|
89
|
+
{ name: 'checksums-happier-server-v1.2.3.txt', browser_download_url: 'https://example.test/checksums.txt' },
|
|
90
|
+
{ name: 'checksums-happier-server-v1.2.3.txt.minisig', browser_download_url: 'https://example.test/checksums.txt.minisig' },
|
|
91
|
+
];
|
|
92
|
+
const picked = pickReleaseAsset({
|
|
93
|
+
assets,
|
|
94
|
+
product: 'happier-server',
|
|
95
|
+
os: 'windows',
|
|
96
|
+
arch: 'x64',
|
|
97
|
+
});
|
|
98
|
+
assert.equal(picked.archiveUrl, 'https://example.test/server.tgz');
|
|
52
99
|
});
|
|
53
100
|
|
|
54
101
|
test('renderServerServiceUnit references configured binary and env file', () => {
|
|
@@ -65,6 +112,19 @@ test('renderServerServiceUnit references configured binary and env file', () =>
|
|
|
65
112
|
assert.match(unit, /StandardOutput=append:\/var\/log\/happier\/server.log/);
|
|
66
113
|
});
|
|
67
114
|
|
|
115
|
+
test('resolveSelfHostDefaults uses user-mode paths by default', () => {
|
|
116
|
+
const cfg = resolveSelfHostDefaults({ platform: 'linux', mode: 'user', homeDir: '/home/me' });
|
|
117
|
+
assert.equal(cfg.installRoot, '/home/me/.happier/self-host');
|
|
118
|
+
assert.equal(cfg.binDir, '/home/me/.happier/bin');
|
|
119
|
+
assert.equal(cfg.configDir, '/home/me/.happier/self-host/config');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('resolveMinisignPublicKeyText prefers inline override and otherwise returns bundled key', () => {
|
|
123
|
+
const bundled = resolveMinisignPublicKeyText({});
|
|
124
|
+
assert.match(bundled, /minisign public key/i);
|
|
125
|
+
assert.equal(resolveMinisignPublicKeyText({ HAPPIER_MINISIGN_PUBKEY: 'hello' }), 'hello');
|
|
126
|
+
});
|
|
127
|
+
|
|
68
128
|
test('renderServerEnvFile emits sqlite/local defaults for self-host mode', () => {
|
|
69
129
|
const envText = renderServerEnvFile({
|
|
70
130
|
port: 3005,
|
|
@@ -74,9 +134,309 @@ test('renderServerEnvFile emits sqlite/local defaults for self-host mode', () =>
|
|
|
74
134
|
dbDir: '/var/lib/happier/pglite',
|
|
75
135
|
});
|
|
76
136
|
assert.match(envText, /PORT=3005/);
|
|
137
|
+
assert.match(envText, /METRICS_ENABLED=false/);
|
|
77
138
|
assert.match(envText, /HAPPIER_DB_PROVIDER=sqlite/);
|
|
139
|
+
assert.match(envText, /DATABASE_URL=file:\/var\/lib\/happier\/happier-server-light\.sqlite/);
|
|
78
140
|
assert.match(envText, /HAPPIER_FILES_BACKEND=local/);
|
|
141
|
+
assert.match(envText, /HAPPIER_SQLITE_AUTO_MIGRATE=1/);
|
|
142
|
+
assert.match(envText, /HAPPIER_SQLITE_MIGRATIONS_DIR=\/var\/lib\/happier\/migrations\/sqlite/);
|
|
79
143
|
assert.match(envText, /HAPPIER_SERVER_LIGHT_DATA_DIR=\/var\/lib\/happier/);
|
|
80
144
|
assert.match(envText, /HAPPIER_SERVER_LIGHT_FILES_DIR=\/var\/lib\/happier\/files/);
|
|
81
145
|
assert.match(envText, /HAPPIER_SERVER_LIGHT_DB_DIR=\/var\/lib\/happier\/pglite/);
|
|
82
146
|
});
|
|
147
|
+
|
|
148
|
+
test('renderServerEnvFile includes ui bundle directory when provided', () => {
|
|
149
|
+
const envText = renderServerEnvFile({
|
|
150
|
+
port: 3005,
|
|
151
|
+
host: '127.0.0.1',
|
|
152
|
+
dataDir: '/var/lib/happier',
|
|
153
|
+
filesDir: '/var/lib/happier/files',
|
|
154
|
+
dbDir: '/var/lib/happier/pglite',
|
|
155
|
+
uiDir: '/var/lib/happier/ui-web/current',
|
|
156
|
+
});
|
|
157
|
+
assert.match(envText, /HAPPIER_SERVER_UI_DIR=\/var\/lib\/happier\/ui-web\/current/);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('renderServerEnvFile includes PRISMA_QUERY_ENGINE_LIBRARY when a packaged sqlite engine is present', async () => {
|
|
161
|
+
const serverBinDir = await mkdtemp(join(tmpdir(), 'happier-self-host-bin-'));
|
|
162
|
+
await mkdir(join(serverBinDir, 'generated', 'sqlite-client'), { recursive: true });
|
|
163
|
+
const enginePath = join(serverBinDir, 'generated', 'sqlite-client', 'libquery_engine-darwin-arm64.dylib.node');
|
|
164
|
+
await writeFile(enginePath, 'stub', 'utf-8');
|
|
165
|
+
|
|
166
|
+
const envText = renderServerEnvFile({
|
|
167
|
+
port: 3005,
|
|
168
|
+
host: '127.0.0.1',
|
|
169
|
+
platform: 'darwin',
|
|
170
|
+
arch: 'arm64',
|
|
171
|
+
serverBinDir,
|
|
172
|
+
dataDir: '/var/lib/happier',
|
|
173
|
+
filesDir: '/var/lib/happier/files',
|
|
174
|
+
dbDir: '/var/lib/happier/pglite',
|
|
175
|
+
});
|
|
176
|
+
assert.match(envText, /PRISMA_CLIENT_ENGINE_TYPE=library/);
|
|
177
|
+
assert.match(envText, new RegExp(`PRISMA_QUERY_ENGINE_LIBRARY=${enginePath.replaceAll('\\\\', '\\\\\\\\')}`));
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
test('renderServerEnvFile uses file URL semantics on Windows', () => {
|
|
181
|
+
const envText = renderServerEnvFile({
|
|
182
|
+
port: 3005,
|
|
183
|
+
host: '127.0.0.1',
|
|
184
|
+
platform: 'win32',
|
|
185
|
+
dataDir: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\data',
|
|
186
|
+
filesDir: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\data\\\\files',
|
|
187
|
+
dbDir: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\data\\\\pglite',
|
|
188
|
+
});
|
|
189
|
+
assert.match(envText, /DATABASE_URL=file:\/\/\/C:\/Users\/me\/\.happier\/self-host\/data\/happier-server-light\.sqlite/);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test('resolveSelfHostHealthTimeoutMs defaults to a safe health timeout', () => {
|
|
193
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({}), 90_000);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test('resolveSelfHostHealthTimeoutMs honors explicit timeout values >= 10s', () => {
|
|
197
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({ HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: '120000' }), 120_000);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('resolveSelfHostHealthTimeoutMs ignores invalid or too-small values', () => {
|
|
201
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({ HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: 'abc' }), 90_000);
|
|
202
|
+
assert.equal(resolveSelfHostHealthTimeoutMs({ HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: '5000' }), 90_000);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('resolveSelfHostAutoUpdateDefault is opt-in (disabled by default)', () => {
|
|
206
|
+
assert.equal(resolveSelfHostAutoUpdateDefault({}), false);
|
|
207
|
+
assert.equal(resolveSelfHostAutoUpdateDefault({ HAPPIER_SELF_HOST_AUTO_UPDATE: '1' }), true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test('resolveSelfHostAutoUpdateIntervalMinutes provides a safe default and bounds invalid values', () => {
|
|
211
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({}), 1440);
|
|
212
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({ HAPPIER_SELF_HOST_AUTO_UPDATE_INTERVAL_MINUTES: '60' }), 60);
|
|
213
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({ HAPPIER_SELF_HOST_AUTO_UPDATE_INTERVAL_MINUTES: '0' }), 1440);
|
|
214
|
+
assert.equal(resolveSelfHostAutoUpdateIntervalMinutes({ HAPPIER_SELF_HOST_AUTO_UPDATE_INTERVAL_MINUTES: 'abc' }), 1440);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test('renderUpdaterSystemdUnit runs self-host update without restart loops', () => {
|
|
218
|
+
const unit = renderUpdaterSystemdUnit({
|
|
219
|
+
updaterLabel: 'happier-server-updater',
|
|
220
|
+
hstackPath: '/home/me/.happier/bin/hstack',
|
|
221
|
+
channel: 'preview',
|
|
222
|
+
mode: 'user',
|
|
223
|
+
workingDirectory: '/home/me/.happier/self-host',
|
|
224
|
+
stdoutPath: '/home/me/.happier/self-host/logs/updater.out.log',
|
|
225
|
+
stderrPath: '/home/me/.happier/self-host/logs/updater.err.log',
|
|
226
|
+
wantedBy: 'default.target',
|
|
227
|
+
});
|
|
228
|
+
assert.match(unit, /ExecStart=\/home\/me\/\.happier\/bin\/hstack self-host update --channel=preview --mode=user --non-interactive/);
|
|
229
|
+
assert.match(unit, /Restart=no/);
|
|
230
|
+
assert.match(unit, /WantedBy=default\.target/);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
test('renderUpdaterLaunchdPlistXml runs self-host update without keepalive loops', () => {
|
|
234
|
+
const plist = renderUpdaterLaunchdPlistXml({
|
|
235
|
+
updaterLabel: 'happier-server-updater',
|
|
236
|
+
hstackPath: '/Users/me/.happier/bin/hstack',
|
|
237
|
+
channel: 'preview',
|
|
238
|
+
mode: 'user',
|
|
239
|
+
intervalMinutes: 60,
|
|
240
|
+
workingDirectory: '/Users/me/.happier/self-host',
|
|
241
|
+
stdoutPath: '/Users/me/.happier/self-host/logs/updater.out.log',
|
|
242
|
+
stderrPath: '/Users/me/.happier/self-host/logs/updater.err.log',
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
assert.match(plist, /<key>RunAtLoad<\/key>\s*<true\/>/);
|
|
246
|
+
assert.match(plist, /<key>StartInterval<\/key>\s*<integer>3600<\/integer>/);
|
|
247
|
+
assert.doesNotMatch(plist, /<key>KeepAlive<\/key>/);
|
|
248
|
+
assert.match(plist, /<key>PATH<\/key>/);
|
|
249
|
+
assert.match(plist, /<string>\/Users\/me\/\.happier\/bin\/hstack<\/string>/);
|
|
250
|
+
assert.match(plist, /<string>self-host<\/string>/);
|
|
251
|
+
assert.match(plist, /<string>update<\/string>/);
|
|
252
|
+
assert.match(plist, /<string>--channel=preview<\/string>/);
|
|
253
|
+
assert.match(plist, /<string>--mode=user<\/string>/);
|
|
254
|
+
assert.match(plist, /<string>--non-interactive<\/string>/);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test('renderUpdaterSystemdTimerUnit schedules periodic updater runs', () => {
|
|
258
|
+
const timer = renderUpdaterSystemdTimerUnit({
|
|
259
|
+
updaterLabel: 'happier-server-updater',
|
|
260
|
+
intervalMinutes: 60,
|
|
261
|
+
});
|
|
262
|
+
assert.match(timer, /OnUnitActiveSec=60m/);
|
|
263
|
+
assert.match(timer, /Unit=happier-server-updater\.service/);
|
|
264
|
+
assert.match(timer, /WantedBy=timers\.target/);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test('renderUpdaterScheduledTaskWrapperPs1 runs self-host update without node dependencies', () => {
|
|
268
|
+
const wrapper = renderUpdaterScheduledTaskWrapperPs1({
|
|
269
|
+
updaterLabel: 'happier-server-updater',
|
|
270
|
+
hstackPath: 'C:\\\\Users\\\\me\\\\.happier\\\\bin\\\\hstack.exe',
|
|
271
|
+
channel: 'preview',
|
|
272
|
+
mode: 'user',
|
|
273
|
+
workingDirectory: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host',
|
|
274
|
+
stdoutPath: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\logs\\\\updater.out.log',
|
|
275
|
+
stderrPath: 'C:\\\\Users\\\\me\\\\.happier\\\\self-host\\\\logs\\\\updater.err.log',
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
assert.match(
|
|
279
|
+
wrapper,
|
|
280
|
+
/hstack\.exe"\s+"self-host"\s+"update"\s+"--channel=preview"\s+"--mode=user"\s+"--non-interactive"/i
|
|
281
|
+
);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
test('mergeEnvTextWithDefaults preserves overrides while backfilling new default keys', () => {
|
|
285
|
+
const defaults = renderServerEnvFile({
|
|
286
|
+
port: 3005,
|
|
287
|
+
host: '127.0.0.1',
|
|
288
|
+
dataDir: '/var/lib/happier',
|
|
289
|
+
filesDir: '/var/lib/happier/files',
|
|
290
|
+
dbDir: '/var/lib/happier/pglite',
|
|
291
|
+
});
|
|
292
|
+
const existing = [
|
|
293
|
+
...defaults
|
|
294
|
+
.split('\n')
|
|
295
|
+
.filter((line) => !line.startsWith('HAPPIER_SQLITE_AUTO_MIGRATE=') && !line.startsWith('HAPPIER_SQLITE_MIGRATIONS_DIR=')),
|
|
296
|
+
'PORT=7777',
|
|
297
|
+
'FOO=bar',
|
|
298
|
+
'',
|
|
299
|
+
].join('\n');
|
|
300
|
+
|
|
301
|
+
const merged = mergeEnvTextWithDefaults(existing, defaults);
|
|
302
|
+
assert.match(merged, /PORT=7777/);
|
|
303
|
+
assert.match(merged, /HAPPIER_SQLITE_AUTO_MIGRATE=1/);
|
|
304
|
+
assert.match(merged, /HAPPIER_SQLITE_MIGRATIONS_DIR=\/var\/lib\/happier\/migrations\/sqlite/);
|
|
305
|
+
assert.match(merged, /FOO=bar/);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test('renderSelfHostStatusText reports versions, health, and auto-update config separately from job state', () => {
|
|
309
|
+
const text = renderSelfHostStatusText(
|
|
310
|
+
{
|
|
311
|
+
channel: 'preview',
|
|
312
|
+
mode: 'user',
|
|
313
|
+
serviceName: 'happier-server',
|
|
314
|
+
serverUrl: 'http://127.0.0.1:3005',
|
|
315
|
+
healthy: true,
|
|
316
|
+
service: { active: true, enabled: true },
|
|
317
|
+
versions: { server: '1.2.3-preview.1', uiWeb: '9.9.9-preview.2' },
|
|
318
|
+
autoUpdate: {
|
|
319
|
+
label: 'happier-server-updater',
|
|
320
|
+
job: { active: true, enabled: true },
|
|
321
|
+
configured: { enabled: true, intervalMinutes: 60 },
|
|
322
|
+
},
|
|
323
|
+
updatedAt: '2026-02-15T00:00:00.000Z',
|
|
324
|
+
},
|
|
325
|
+
{ colors: false },
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
assert.match(text, /channel:\s*preview/);
|
|
329
|
+
assert.match(text, /mode:\s*user/);
|
|
330
|
+
assert.match(text, /url:\s*http:\/\/127\.0\.0\.1:3005/);
|
|
331
|
+
assert.match(text, /health:\s*ok/);
|
|
332
|
+
assert.match(text, /server:\s*1\.2\.3-preview\.1/);
|
|
333
|
+
assert.match(text, /ui-web:\s*9\.9\.9-preview\.2/);
|
|
334
|
+
assert.match(text, /auto-update:\s*configured enabled \(every 60m\); job enabled, active/);
|
|
335
|
+
assert.match(text, /updated:\s*2026-02-15T00:00:00\.000Z/);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('renderSelfHostStatusText shows disabled auto-update config even if job state is unknown', () => {
|
|
339
|
+
const text = renderSelfHostStatusText(
|
|
340
|
+
{
|
|
341
|
+
channel: 'stable',
|
|
342
|
+
mode: 'user',
|
|
343
|
+
serviceName: 'happier-server',
|
|
344
|
+
serverUrl: 'http://127.0.0.1:3005',
|
|
345
|
+
healthy: false,
|
|
346
|
+
service: { active: null, enabled: null },
|
|
347
|
+
versions: { server: null, uiWeb: null },
|
|
348
|
+
autoUpdate: {
|
|
349
|
+
label: 'happier-server-updater',
|
|
350
|
+
job: { active: null, enabled: null },
|
|
351
|
+
configured: { enabled: false, intervalMinutes: 1440 },
|
|
352
|
+
},
|
|
353
|
+
updatedAt: null,
|
|
354
|
+
},
|
|
355
|
+
{ colors: false },
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
assert.match(text, /auto-update:\s*configured disabled; job unknown/);
|
|
359
|
+
assert.match(text, /health:\s*failed/);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
test('buildSelfHostDoctorChecks does not require external minisign and includes ui-web checks when installed', () => {
|
|
363
|
+
const checks = buildSelfHostDoctorChecks(
|
|
364
|
+
{
|
|
365
|
+
platform: 'linux',
|
|
366
|
+
mode: 'user',
|
|
367
|
+
serverBinaryPath: '/home/me/.happier/self-host/bin/happier-server',
|
|
368
|
+
configEnvPath: '/home/me/.happier/self-host/config/server.env',
|
|
369
|
+
uiWebCurrentDir: '/home/me/.happier/self-host/ui-web/current',
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
state: { uiWeb: { installed: true } },
|
|
373
|
+
commandExists: (name) => new Set(['tar', 'systemctl']).has(name),
|
|
374
|
+
pathExists: (p) => p.endsWith('happier-server') || p.endsWith('server.env') || p.endsWith('index.html'),
|
|
375
|
+
},
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
assert.ok(checks.find((c) => c.name === 'tar')?.ok);
|
|
379
|
+
assert.ok(checks.find((c) => c.name === 'systemctl')?.ok);
|
|
380
|
+
assert.equal(checks.some((c) => c.name === 'minisign'), false);
|
|
381
|
+
assert.ok(checks.find((c) => c.name === 'ui-web')?.ok);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('buildSelfHostDoctorChecks flags missing ui-web bundle when state expects ui-web installed', () => {
|
|
385
|
+
const checks = buildSelfHostDoctorChecks(
|
|
386
|
+
{
|
|
387
|
+
platform: 'linux',
|
|
388
|
+
mode: 'user',
|
|
389
|
+
serverBinaryPath: '/home/me/.happier/self-host/bin/happier-server',
|
|
390
|
+
configEnvPath: '/home/me/.happier/self-host/config/server.env',
|
|
391
|
+
uiWebCurrentDir: '/home/me/.happier/self-host/ui-web/current',
|
|
392
|
+
},
|
|
393
|
+
{
|
|
394
|
+
state: { uiWeb: { installed: true } },
|
|
395
|
+
commandExists: () => true,
|
|
396
|
+
pathExists: (p) => !p.endsWith('index.html'),
|
|
397
|
+
},
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
assert.equal(checks.find((c) => c.name === 'ui-web')?.ok, false);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test('normalizeSelfHostAutoUpdateState upgrades legacy boolean config to structured config', () => {
|
|
404
|
+
assert.deepEqual(
|
|
405
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: true }, { fallbackIntervalMinutes: 1440 }),
|
|
406
|
+
{ enabled: true, intervalMinutes: 1440 },
|
|
407
|
+
);
|
|
408
|
+
assert.deepEqual(
|
|
409
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: false }, { fallbackIntervalMinutes: 1440 }),
|
|
410
|
+
{ enabled: false, intervalMinutes: 1440 },
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
test('normalizeSelfHostAutoUpdateState preserves explicit interval and bounds invalid values', () => {
|
|
415
|
+
assert.deepEqual(
|
|
416
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: { enabled: true, intervalMinutes: 60 } }, { fallbackIntervalMinutes: 1440 }),
|
|
417
|
+
{ enabled: true, intervalMinutes: 60 },
|
|
418
|
+
);
|
|
419
|
+
assert.deepEqual(
|
|
420
|
+
normalizeSelfHostAutoUpdateState({ autoUpdate: { enabled: true, intervalMinutes: 0 } }, { fallbackIntervalMinutes: 1440 }),
|
|
421
|
+
{ enabled: true, intervalMinutes: 1440 },
|
|
422
|
+
);
|
|
423
|
+
assert.deepEqual(
|
|
424
|
+
normalizeSelfHostAutoUpdateState({}, { fallbackIntervalMinutes: 1440 }),
|
|
425
|
+
{ enabled: false, intervalMinutes: 1440 },
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
test('decideSelfHostAutoUpdateReconcile maps configured state to an install/uninstall action', () => {
|
|
430
|
+
assert.deepEqual(
|
|
431
|
+
decideSelfHostAutoUpdateReconcile({ autoUpdate: true }, { fallbackIntervalMinutes: 1440 }),
|
|
432
|
+
{ action: 'install', enabled: true, intervalMinutes: 1440 },
|
|
433
|
+
);
|
|
434
|
+
assert.deepEqual(
|
|
435
|
+
decideSelfHostAutoUpdateReconcile({ autoUpdate: false }, { fallbackIntervalMinutes: 1440 }),
|
|
436
|
+
{ action: 'uninstall', enabled: false, intervalMinutes: 1440 },
|
|
437
|
+
);
|
|
438
|
+
assert.deepEqual(
|
|
439
|
+
decideSelfHostAutoUpdateReconcile({}, { fallbackIntervalMinutes: 1440 }),
|
|
440
|
+
{ action: 'uninstall', enabled: false, intervalMinutes: 1440 },
|
|
441
|
+
);
|
|
442
|
+
});
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { cp, mkdir, mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const SELF_HOST_INSTALL_TIMEOUT_MS = 420_000;
|
|
9
|
+
|
|
10
|
+
import { commandExists, extractBinaryFromArtifact, reserveLocalhostPort, run, waitForHealth } from './self_host_service_e2e_harness.mjs';
|
|
11
|
+
|
|
12
|
+
function currentTarget() {
|
|
13
|
+
if (process.platform !== 'win32') return '';
|
|
14
|
+
if (process.arch === 'x64') return 'windows-x64';
|
|
15
|
+
return '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function readTail(path) {
|
|
19
|
+
const escaped = String(path ?? '').replaceAll("'", "''");
|
|
20
|
+
return run(
|
|
21
|
+
'powershell',
|
|
22
|
+
['-NoProfile', '-Command', `Get-Content -LiteralPath '${escaped}' -Tail 200 -ErrorAction SilentlyContinue`],
|
|
23
|
+
{ label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
test(
|
|
28
|
+
'compiled hstack self-host install/uninstall works on Windows schtasks host without repo checkout',
|
|
29
|
+
{ timeout: 15 * 60_000 },
|
|
30
|
+
async (t) => {
|
|
31
|
+
if (process.platform !== 'win32') {
|
|
32
|
+
t.skip(`windows-only test (current: ${process.platform})`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const target = currentTarget();
|
|
36
|
+
if (!target) {
|
|
37
|
+
t.skip(`unsupported Windows runner architecture: ${process.arch}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (!commandExists('schtasks')) {
|
|
41
|
+
t.skip('schtasks is required');
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (!commandExists('powershell')) {
|
|
45
|
+
t.skip('powershell is required');
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (!commandExists('bun')) {
|
|
49
|
+
t.skip('bun is required to build compiled binaries');
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const repoRoot = resolve(fileURLToPath(new URL('../../..', import.meta.url)));
|
|
54
|
+
const version = `0.0.0-schtasks.${Date.now()}`;
|
|
55
|
+
|
|
56
|
+
run(
|
|
57
|
+
process.execPath,
|
|
58
|
+
[
|
|
59
|
+
'scripts/release/build-hstack-binaries.mjs',
|
|
60
|
+
'--channel=preview',
|
|
61
|
+
`--version=${version}`,
|
|
62
|
+
`--targets=${target}`,
|
|
63
|
+
],
|
|
64
|
+
{
|
|
65
|
+
label: 'self-host-schtasks',
|
|
66
|
+
cwd: repoRoot,
|
|
67
|
+
env: { ...process.env },
|
|
68
|
+
timeoutMs: 10 * 60_000,
|
|
69
|
+
}
|
|
70
|
+
);
|
|
71
|
+
run(
|
|
72
|
+
process.execPath,
|
|
73
|
+
[
|
|
74
|
+
'scripts/release/build-server-binaries.mjs',
|
|
75
|
+
'--channel=preview',
|
|
76
|
+
`--version=${version}`,
|
|
77
|
+
`--targets=${target}`,
|
|
78
|
+
],
|
|
79
|
+
{
|
|
80
|
+
label: 'self-host-schtasks',
|
|
81
|
+
cwd: repoRoot,
|
|
82
|
+
env: { ...process.env },
|
|
83
|
+
timeoutMs: 10 * 60_000,
|
|
84
|
+
}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const hstackArtifact = join(repoRoot, 'dist', 'release-assets', 'stack', `hstack-v${version}-${target}.tar.gz`);
|
|
88
|
+
const serverArtifact = join(repoRoot, 'dist', 'release-assets', 'server', `happier-server-v${version}-${target}.tar.gz`);
|
|
89
|
+
|
|
90
|
+
const extractedHstack = await extractBinaryFromArtifact({ label: 'self-host-schtasks', artifactPath: hstackArtifact, binaryName: 'hstack.exe' });
|
|
91
|
+
const extractedServer = await extractBinaryFromArtifact({ label: 'self-host-schtasks', artifactPath: serverArtifact, binaryName: 'happier-server.exe' });
|
|
92
|
+
|
|
93
|
+
t.after(async () => {
|
|
94
|
+
await rm(extractedHstack.extractDir, { recursive: true, force: true });
|
|
95
|
+
await rm(extractedServer.extractDir, { recursive: true, force: true });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
const sandboxDir = await mkdtemp(join(tmpdir(), 'happier-self-host-schtasks-'));
|
|
99
|
+
t.after(async () => {
|
|
100
|
+
await rm(sandboxDir, { recursive: true, force: true });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const installRoot = join(sandboxDir, 'self-host');
|
|
104
|
+
const binDir = join(sandboxDir, 'bin');
|
|
105
|
+
const configDir = join(sandboxDir, 'config');
|
|
106
|
+
const dataDir = join(sandboxDir, 'data');
|
|
107
|
+
const logDir = join(sandboxDir, 'logs');
|
|
108
|
+
await mkdir(binDir, { recursive: true });
|
|
109
|
+
|
|
110
|
+
const hstackPath = join(binDir, 'hstack.exe');
|
|
111
|
+
await cp(extractedHstack.binaryPath, hstackPath);
|
|
112
|
+
|
|
113
|
+
const serviceName = `happier-server-e2e-${Date.now().toString(36).slice(-6)}`;
|
|
114
|
+
const serverPort = await reserveLocalhostPort();
|
|
115
|
+
const commonEnv = {
|
|
116
|
+
PATH: process.env.PATH ?? '',
|
|
117
|
+
HAPPIER_SELF_HOST_INSTALL_ROOT: installRoot,
|
|
118
|
+
HAPPIER_SELF_HOST_BIN_DIR: binDir,
|
|
119
|
+
HAPPIER_SELF_HOST_CONFIG_DIR: configDir,
|
|
120
|
+
HAPPIER_SELF_HOST_DATA_DIR: dataDir,
|
|
121
|
+
HAPPIER_SELF_HOST_LOG_DIR: logDir,
|
|
122
|
+
HAPPIER_SELF_HOST_SERVICE_NAME: serviceName,
|
|
123
|
+
HAPPIER_SELF_HOST_SERVER_BINARY: extractedServer.binaryPath,
|
|
124
|
+
HAPPIER_SELF_HOST_AUTO_UPDATE: '0',
|
|
125
|
+
HAPPIER_SELF_HOST_HEALTH_TIMEOUT_MS: '240000',
|
|
126
|
+
HAPPIER_NONINTERACTIVE: '1',
|
|
127
|
+
HAPPIER_WITH_CLI: '0',
|
|
128
|
+
HAPPIER_SERVER_PORT: String(serverPort),
|
|
129
|
+
HAPPIER_SERVER_HOST: '127.0.0.1',
|
|
130
|
+
};
|
|
131
|
+
const serverOutLog = join(logDir, 'server.out.log');
|
|
132
|
+
const serverErrLog = join(logDir, 'server.err.log');
|
|
133
|
+
|
|
134
|
+
const taskName = `Happier\\${serviceName}`;
|
|
135
|
+
|
|
136
|
+
let installSucceeded = false;
|
|
137
|
+
t.after(() => {
|
|
138
|
+
if (!installSucceeded) return;
|
|
139
|
+
run(
|
|
140
|
+
hstackPath,
|
|
141
|
+
['self-host', 'uninstall', '--channel=preview', '--mode=user', '--yes', '--purge-data', '--json'],
|
|
142
|
+
{
|
|
143
|
+
env: commonEnv,
|
|
144
|
+
allowFail: true,
|
|
145
|
+
timeoutMs: 180_000,
|
|
146
|
+
stdio: 'ignore',
|
|
147
|
+
cwd: sandboxDir,
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
const installResult = run(
|
|
153
|
+
hstackPath,
|
|
154
|
+
['self-host', 'install', '--channel=preview', '--mode=user', '--no-auto-update', '--non-interactive', '--without-cli', '--json'],
|
|
155
|
+
{
|
|
156
|
+
label: 'self-host-schtasks',
|
|
157
|
+
env: commonEnv,
|
|
158
|
+
timeoutMs: SELF_HOST_INSTALL_TIMEOUT_MS,
|
|
159
|
+
allowFail: true,
|
|
160
|
+
cwd: sandboxDir,
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
if ((installResult.status ?? 1) !== 0) {
|
|
164
|
+
const recoveredHealth = await waitForHealth(`http://127.0.0.1:${serverPort}/v1/version`, 120_000);
|
|
165
|
+
if (!recoveredHealth) {
|
|
166
|
+
const statusResult = run(
|
|
167
|
+
hstackPath,
|
|
168
|
+
['self-host', 'status', '--channel=preview', '--mode=user', '--json'],
|
|
169
|
+
{ label: 'self-host-schtasks', env: commonEnv, allowFail: true, timeoutMs: 60_000, cwd: sandboxDir }
|
|
170
|
+
);
|
|
171
|
+
const schtasksQuery = run('schtasks', ['/Query', '/TN', taskName, '/FO', 'LIST', '/V'], { label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 });
|
|
172
|
+
const outTail = readTail(serverOutLog);
|
|
173
|
+
const errTail = readTail(serverErrLog);
|
|
174
|
+
throw new Error(
|
|
175
|
+
[
|
|
176
|
+
'[self-host-schtasks] self-host install failed and service never became healthy',
|
|
177
|
+
`install status: ${String(installResult.status ?? 'null')}`,
|
|
178
|
+
`install stdout:\n${String(installResult.stdout ?? '').trim()}`,
|
|
179
|
+
`install stderr:\n${String(installResult.stderr ?? '').trim()}`,
|
|
180
|
+
`self-host status:\n${String(statusResult.stdout ?? '').trim()}\n${String(statusResult.stderr ?? '').trim()}`,
|
|
181
|
+
`schtasks query (${taskName}):\n${String(schtasksQuery.stdout ?? '').trim()}\n${String(schtasksQuery.stderr ?? '').trim()}`,
|
|
182
|
+
`server out tail (${serverOutLog}):\n${String(outTail.stdout ?? '').trim()}\n${String(outTail.stderr ?? '').trim()}`,
|
|
183
|
+
`server err tail (${serverErrLog}):\n${String(errTail.stdout ?? '').trim()}\n${String(errTail.stderr ?? '').trim()}`,
|
|
184
|
+
].join('\n\n')
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
installSucceeded = true;
|
|
189
|
+
|
|
190
|
+
const healthOk = await waitForHealth(`http://127.0.0.1:${serverPort}/v1/version`, 120_000);
|
|
191
|
+
assert.equal(healthOk, true, 'self-host service health endpoint did not become ready');
|
|
192
|
+
|
|
193
|
+
const status = run(
|
|
194
|
+
hstackPath,
|
|
195
|
+
['self-host', 'status', '--channel=preview', '--mode=user', '--json'],
|
|
196
|
+
{ label: 'self-host-schtasks', env: commonEnv, timeoutMs: 60_000, cwd: sandboxDir }
|
|
197
|
+
);
|
|
198
|
+
const statusPayload = JSON.parse(String(status.stdout ?? '').trim());
|
|
199
|
+
assert.equal(statusPayload?.ok, true);
|
|
200
|
+
assert.equal(statusPayload?.service?.name, serviceName);
|
|
201
|
+
assert.equal(statusPayload?.service?.active, true);
|
|
202
|
+
assert.equal(statusPayload?.healthy, true);
|
|
203
|
+
|
|
204
|
+
const schtasksQueryAfter = run('schtasks', ['/Query', '/TN', taskName], { label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 });
|
|
205
|
+
assert.equal(schtasksQueryAfter.status, 0, 'schtasks query should succeed after install');
|
|
206
|
+
|
|
207
|
+
run(
|
|
208
|
+
hstackPath,
|
|
209
|
+
['self-host', 'uninstall', '--channel=preview', '--mode=user', '--yes', '--purge-data', '--json'],
|
|
210
|
+
{ label: 'self-host-schtasks', env: commonEnv, timeoutMs: 180_000, cwd: sandboxDir }
|
|
211
|
+
);
|
|
212
|
+
installSucceeded = false;
|
|
213
|
+
|
|
214
|
+
const schtasksAfterUninstall = run('schtasks', ['/Query', '/TN', taskName], { label: 'self-host-schtasks', allowFail: true, timeoutMs: 20_000 });
|
|
215
|
+
assert.notEqual(schtasksAfterUninstall.status, 0, 'scheduled task should not remain after uninstall');
|
|
216
|
+
}
|
|
217
|
+
);
|