flow run: a declined confirmation stops the flow; --apply --yes confirms write steps
confirm() prints 'declined: <reason>' and exits 3 whenever the answer is no or nobody can answer (not a terminal without --yes, serve without the owner's dialog), so no command reports success after declining. flow run stops at such a step with 'step N declined: <reason>' (status declined), never ok. flow run --apply --yes passes --yes to write steps only; --yes without --apply is refused. Pitfall 24.
This commit is contained in:
parent
5fbb9e63e3
commit
366362d62b
12 changed files with 116 additions and 19 deletions
|
|
@ -35,6 +35,7 @@ beforeEach(() => {
|
|||
Object.defineProperty(process.stdin, 'isTTY', { value: false, configurable: true });
|
||||
});
|
||||
afterEach(() => {
|
||||
process.exitCode = 0; // a declined confirmation sets it (DECLINED_EXIT); the tests below check it
|
||||
globalThis.fetch = realFetch;
|
||||
for (const [k, v] of Object.entries(saved)) { if (v === undefined) delete process.env[k]; else process.env[k] = v; }
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
|
|
@ -236,12 +237,14 @@ describe('command (mocked fetch)', () => {
|
|||
expect(out).toContain('100.00 -> 150.00 USD (+50%)');
|
||||
expect(out).toContain('not a terminal');
|
||||
expect(calls.some(c => c.method === 'PUT')).toBe(false);
|
||||
expect(process.exitCode).toBe(3);
|
||||
});
|
||||
test('pause under storebound serve is refused even with --yes', async () => {
|
||||
process.env.STOREBOUND_SERVE = '1';
|
||||
const calls = mockFetch(api);
|
||||
await quiet(() => run(ctx(['pause', '--campaign', '444555666', '--yes'])));
|
||||
expect(calls.some(c => c.method === 'PUT')).toBe(false);
|
||||
expect(process.exitCode).toBe(3);
|
||||
});
|
||||
test('budget with --yes in a terminal-less run sends the documented body', async () => {
|
||||
const calls = mockFetch(api);
|
||||
|
|
|
|||
|
|
@ -83,6 +83,12 @@ describe('steps', () => {
|
|||
expect(planStep({ id: 'sh', sh: 'npm run build' }, 'steps', 'apply', vars, C).skip).toBeUndefined();
|
||||
expect(planStep({ id: 'p', run: 'play build', if: 'play' }, 'steps', 'apply', vars, C).skip).toBe('if play: false');
|
||||
expect(planStep({ id: 'n', run: 'whats-new apply', if: 'exists store/whats-new' }, 'steps', 'apply', vars, C).skip).toBeUndefined();
|
||||
// flow run --apply --yes: the owner's approval reaches write steps only (never reads, waits or owner steps)
|
||||
expect(planStep(up, 'steps', 'apply', vars, C, {}, true)).toMatchObject({ argv: ['apple', 'upload', '--no-wait', '--yes'], yesAdded: true });
|
||||
expect(planStep(up, 'steps', 'dry', vars, C, {}, true).argv).toEqual(['apple', 'upload', '--no-wait', '--dry']);
|
||||
expect(planStep({ id: 'v', run: 'validate' }, 'steps', 'apply', vars, C, {}, true).argv).toEqual(['validate']);
|
||||
expect(planStep({ id: 'w', run: 'apple testflight wait --build 1' }, 'steps', 'apply', vars, C, {}, true).argv).not.toContain('--yes');
|
||||
expect(planStep({ id: 'r', run: 'apple release' }, 'steps', 'apply', vars, C, {}, true)).toMatchObject({ kind: 'owner', skip: expect.stringContaining('your click') });
|
||||
});
|
||||
test('conditions, variables, params', () => {
|
||||
expect(evalIf('apple && !play', C)).toBe(true);
|
||||
|
|
@ -191,6 +197,27 @@ steps:
|
|||
expect(sb(app, 'flow', 'run', 'nope').stderr.toString()).toContain('no flow "nope"');
|
||||
});
|
||||
|
||||
test('a declined confirmation (not a terminal, no --yes) stops the flow at that step, never "ok"', () => {
|
||||
const d = join(tmp, 'declined');
|
||||
mkdirSync(join(d, 'store', 'workflows'), { recursive: true });
|
||||
writeFileSync(join(d, 'storebound.config.ts'), `export default { app: { name: 'Example', privacyPolicyUrl: 'https://example.com/p', supportUrl: 'https://example.com', website: 'https://example.com', contactEmail: 'hi@example.com' },
|
||||
languages: [{ id: 'en', play: 'en-US' }], play: { packageName: 'com.example.game' } };\n`);
|
||||
writeFileSync(join(d, 'app.aab'), 'not really a bundle');
|
||||
writeFileSync(join(d, 'store', 'workflows', 'share.yml'), 'steps:\n - id: share\n run: play share --aab app.aab\n - id: after\n sh: touch after.txt\n');
|
||||
const direct = sb(d, 'play', 'share', '--aab', 'app.aab');
|
||||
expect(direct.exitCode).not.toBe(0);
|
||||
expect(direct.stderr.toString()).toContain('declined: not a terminal and no --yes');
|
||||
const r = sb(d, 'flow', 'run', 'share', '--apply');
|
||||
const out = r.stdout.toString() + r.stderr.toString();
|
||||
expect(r.exitCode).toBe(1);
|
||||
expect(out).toContain('step 1 declined: not a terminal and no --yes');
|
||||
expect(out).not.toContain('share: ok');
|
||||
expect(existsSync(join(d, 'after.txt'))).toBe(false);
|
||||
const state = JSON.parse(readFileSync(join(d, '.storebound/flows/share.json'), 'utf8'));
|
||||
expect(state.results.map((x: { id: string; status: string }) => `${x.id}:${x.status}`)).toEqual(['share:declined']);
|
||||
expect(sb(d, 'flow', 'run', 'share', '--yes').stderr.toString()).toContain('--yes only with --apply');
|
||||
});
|
||||
|
||||
test('MCP: flow_list and a dry flow_run that never applies', () => {
|
||||
const list = TOOLS.find(t => t.name === 'flow_list')!;
|
||||
const run = TOOLS.find(t => t.name === 'flow_run')!;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,10 @@
|
|||
// Loaded before every test file (bunfig.toml): no real waiting between the re-checks of cli/lib/upsert.ts or for
|
||||
// the store's list to settle after a create (cli/apple/iap.ts).
|
||||
import { afterEach } from 'bun:test';
|
||||
|
||||
process.env.STOREBOUND_UPSERT_WAIT_MS = '0';
|
||||
process.env.STOREBOUND_LIST_SETTLE_MS ??= '0';
|
||||
|
||||
// Commands run in-process set process.exitCode on purpose (a check with problems, a declined confirmation:
|
||||
// DECLINED_EXIT in cli/lib/ctx.ts). Tests assert it where it matters; it must not leak into bun test's own exit code.
|
||||
afterEach(() => { process.exitCode = 0; });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue