apple submit --build <n> (first submission, the resubmit steps; plan table with --json under serve); desktop owner actions to send for review: owner.apple_submit, owner.apple_resubmit, owner.apple_iap_submit, owner.play_production, and the price gate owner actions (owner.prices_play_products / _play_subscriptions / _apple_subscriptions; owner.prices_import passes --allow-price-changes after the dialog); OWNER_ALLOWED_ARGS ties each forbidden flag to its one action, dry runs never carry owner flags, MCP still has no submit or production release; submit dialog text in the host; system.tools androidSdk, project.inspect playRelease, declarations dataDeletion

This commit is contained in:
CRTkafa 2026-10-12 12:07:55 +08:00
parent 2e7dd44ae6
commit 33eb24a3dc
17 changed files with 335 additions and 49 deletions

View file

@ -13,6 +13,7 @@ import { notReadyYet, otherSubmissionOf, submitTiming } from '../cli/apple/submi
import { run as appleIapSubmit } from '../cli/apple/iap-submit';
import { run as appleResubmit } from '../cli/apple/resubmit';
import { run as appleStatus } from '../cli/apple/status';
import { run as appleSubmit } from '../cli/apple/submit';
import { ascEnv, BASE, capture, project, restore } from './helpers/store-mock';
import { installStores, type Stores, uninstallStores, writes } from './helpers/fake-stores';
@ -177,6 +178,62 @@ describe('apple resubmit --build <n>', () => {
});
});
describe('apple submit --build <n> (a first submission, the desktop owner action)', () => {
/** Version 1.0.0 in Prepare for Submission without a build, build 8 VALID, the gems IAP ready, no open submission. */
function fresh(): void {
s.asc.put({ type: 'appStoreVersions', id: 'ver-1', attributes: { versionString: '1.0.0', appVersionState: 'PREPARE_FOR_SUBMISSION', platform: 'IOS' }, rels: { app: APP } });
s.asc.put({ type: 'builds', id: 'b-8', attributes: { version: '8', processingState: 'VALID', usesNonExemptEncryption: false }, rels: { app: APP } });
s.asc.put({ type: 'inAppPurchases', id: 'iap-gems', attributes: { productId: 'com.example.notes.gems', inAppPurchaseType: 'CONSUMABLE', state: 'READY_TO_SUBMIT' }, rels: { app: APP } });
s.asc.put({ type: 'inAppPurchaseVersions', id: 'iapv-gems', attributes: { version: 1, state: 'PREPARE_FOR_SUBMISSION' }, rels: { inAppPurchase: 'iap-gems' } });
}
const ONE: ShipConfig = { ...CONFIG, products: CONFIG.products!.slice(0, 1) };
test('dry run: plan table (build attach, version + IAP), nothing sent; under serve the table comes with the --json result', async () => {
fresh();
const p = project(ONE);
const a = s.calls.length;
const r = await capture(() => appleSubmit(p.ctx(['--build', '8'])));
expect(r.exit).toBeFalsy();
expect(shown(a)).toEqual([]);
expect(r.out).toMatch(/build\s+build 8\s+none attached\s+attach build 8/);
expect(r.out).toMatch(/submission\s+new draft\s+-\s+create/);
expect(r.out).toMatch(/app version 1\.0\.0/);
expect(r.out).toContain('com.example.notes.gems');
expect(r.out).toContain('Submitting is the owner');
process.env.STOREBOUND_MACHINE = '1';
try {
const j = await capture(() => appleSubmit(p.ctx(['--build', '8', '--json'])));
expect(j.out).toContain('STEP');
const line = j.out.split('\n').find(l => l.includes('storebound-result'))!;
expect(JSON.parse(line.slice(line.indexOf('{')))).toMatchObject({ dryRun: true, version: { versionString: '1.0.0', state: 'PREPARE_FOR_SUBMISSION' }, build: { action: 'attach', id: 'b-8' }, blockers: [] });
} finally { delete process.env.STOREBOUND_MACHINE; }
expect(shown(a)).toEqual([]);
});
test('--yes: build attached, one draft with the version and the IAP, submitted; a re-run sends nothing', async () => {
fresh();
const p = project(ONE);
const a = s.calls.length;
const r = await capture(() => appleSubmit(p.ctx(['--build', '8', '--yes'])));
expect([r.exit ?? 0, r.err]).toEqual([0, '']);
const sid = s.asc.all('reviewSubmissions')[0]!.id;
expect(shown(a)).toEqual(['PATCH /v1/appStoreVersions/ver-1/relationships/build', 'POST /v1/reviewSubmissions', 'POST /v1/reviewSubmissionItems', 'POST /v1/reviewSubmissionItems', `PATCH /v1/reviewSubmissions/${sid}`]);
expect(s.asc.all('reviewSubmissions').map(x => x.attributes.state)).toEqual(['WAITING_FOR_REVIEW']);
const b = s.calls.length;
const again = await capture(() => appleSubmit(p.ctx(['--build', '8', '--yes'])));
expect(again.exit).toBeFalsy();
expect(shown(b)).toEqual([]);
});
test('a live version is not submitted; no editable version is a clear stop', async () => {
fresh();
s.asc.res.get('ver-1')!.attributes.appVersionState = 'READY_FOR_SALE';
const p = project(ONE);
await expect(appleSubmit(p.ctx(['--build', '8', '--version', '1.0.0']))).rejects.toThrow(/READY_FOR_SALE: only a version in Prepare for Submission/);
await expect(appleSubmit(p.ctx(['--build', '8']))).rejects.toThrow(/no App Store version to submit/);
});
});
describe('apple iap-submit with a submission back from App Review', () => {
test('--with-version --yes reuses UNRESOLVED_ISSUES: no new draft, the missing item only, then resubmitted; a re-run writes nothing', async () => {
rejected({ ver: 'READY_FOR_REVIEW' }); // the version item was resolved already