app store: run a sidecar's setup.sh, streaming it and collecting what it returns
Implements the half of the template contract that faces the platform: answers go in as environment and never as prompts, and results come back as OFFICER_RESULT_<KEY>= lines on stdout. A line protocol rather than JSON because the same stream is the user's live log — it goes to a terminal panel while the install runs. A script that must emit clean JSON cannot also narrate, and one that emits both needs a framing convention anyway. This mirrors the @@officer:progress@@ sentinel the job runner already uses, with the same rule: marker lines are plucked out, everything else passes through. parseResults is pure and tested against the realistic near-misses: a line that MENTIONS the prefix without starting with it, an empty value (Transmission with no RPC auth returns exactly that, and blank is a real answer), a value containing `=` (splitting on every one would truncate a credential), and a prefix with no assignment (a script bug — skipped rather than stored as a blank key). Verified end to end against a real script: environment reaches it, stderr is forwarded (docker compose writes its progress there, so dropping it would hide most of what a user watches), OFFICER_NONINTERACTIVE is set so a script that would block fails loudly instead of hanging behind a web form, and a non-zero exit is reported with the tail. Notes an artifact rather than hiding it: the two streams are pumped concurrently, so the error tail can interleave differently from real time. The live log is correctly ordered; only the summary can read out of order. Serialising the pumps would make a script that writes heavily to one stream block on the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
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' });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user