Email installs end to end now, which was the point of picking it as tier one: no container, no external
wiring, so the machinery is exercised without the provisioning half.
Verified against the running system, not asserted:
POST /api/app-store/email/install -> {"status":"installed","completed":["preflight","schema","process"]}
row -> email mode=config status=installed enabled=true
pm2 -> officer-email online
second install -> all three steps skipped, process not restarted
disable -> stopped
The server boots with the new router, which is the real test of the capability entry: totality.ts throws
before serve() if a mounted router has none, so booting IS the check passing.
pm2.ts shells out rather than importing pm2 as a library. PM2 is already the supervisor and the
ecosystem file is already the definition of how each process runs; a second thing in charge of that
means two supervisors disagreeing. It also means an owner can undo anything the app store did with a
command they already know. The one fact that matters: `pm2 start <name>` fails for a process PM2 has
never seen, so a first install starts from the ecosystem file with --only, and everything after goes by
name. Callers cannot know which case they are in, so startProcess decides.
Disable stops rather than deletes: a stopped process still shows in `pm2 list`, which is the honest
picture. Deleting would make a disabled sidecar indistinguishable from one never installed.
beginInstall returns the existing row instead of replacing it — that is what makes a retry a resume
rather than a re-provision — and clears lastError on the way in, so a UI never shows a stale failure
beside a working service.
The container half of enable/disable/uninstall is deliberately absent rather than stubbed silently: a
disable that leaves Immich running is a different thing from one that stops it, and the difference is
memory on the user's machine.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
58 lines
2.5 KiB
TypeScript
58 lines
2.5 KiB
TypeScript
import { describe, expect, it } from 'bun:test';
|
|
import { parseResults } from './run-script';
|
|
|
|
// The line protocol between a setup script and the installer. Pure, so it is tested without spawning
|
|
// anything — and worth testing precisely, because a script's output is interleaved with `docker
|
|
// compose`'s and a loose parser would pick up things that merely look like results.
|
|
|
|
describe('parseResults', () => {
|
|
it('reads the values a script hands back', () => {
|
|
const out = parseResults(
|
|
[
|
|
'==> Starting',
|
|
'OFFICER_RESULT_URL=http://127.0.0.1:18091',
|
|
'OFFICER_RESULT_PATH=/transmission/rpc',
|
|
'==> Done',
|
|
].join('\n'),
|
|
);
|
|
expect(out).toEqual({ url: 'http://127.0.0.1:18091', path: '/transmission/rpc' });
|
|
});
|
|
|
|
it('ignores everything that is not a result line', () => {
|
|
// The same stream is the user's live log. Narration must never be mistaken for a value.
|
|
const out = parseResults(
|
|
['Container officer-transmission Started', 'note: OFFICER_RESULT_URL is printed at the end', ''].join('\n'),
|
|
);
|
|
// The middle line MENTIONS the prefix but does not start with it, which is the realistic near-miss.
|
|
expect(out).toEqual({});
|
|
});
|
|
|
|
it('keeps an empty value, because blank is a real answer', () => {
|
|
// Transmission with no RPC auth returns exactly this, and it means "no username", which is
|
|
// different from the key being absent.
|
|
expect(parseResults('OFFICER_RESULT_USERNAME=')).toEqual({ username: '' });
|
|
});
|
|
|
|
it('keeps everything after the first `=`', () => {
|
|
// Tokens and URLs contain `=`; splitting on every one would silently truncate a credential.
|
|
expect(parseResults('OFFICER_RESULT_SECRET=abc=def==')).toEqual({ secret: 'abc=def==' });
|
|
});
|
|
|
|
it('lets a later line win', () => {
|
|
// A script that reports twice has changed its mind — a retry inside it that finally worked.
|
|
expect(parseResults(['OFFICER_RESULT_URL=http://first', 'OFFICER_RESULT_URL=http://second'].join('\n'))).toEqual({
|
|
url: 'http://second',
|
|
});
|
|
});
|
|
|
|
it('skips a prefix with no assignment rather than storing a blank key', () => {
|
|
// A script bug. Storing `{'': ''}` would look like a deliberate empty value downstream.
|
|
expect(parseResults('OFFICER_RESULT_')).toEqual({});
|
|
expect(parseResults('OFFICER_RESULT_=novalue')).toEqual({});
|
|
});
|
|
|
|
it('tolerates indentation, since stderr lines arrive prefixed', () => {
|
|
expect(parseResults(' OFFICER_RESULT_URL=http://x ')).toEqual({ url: 'http://x' });
|
|
});
|
|
});
|