Skip to content

Commit def279a

Browse files
fix(cli): os package publish sends visibility only when --visibility is passed (#20915)
Part of #20892 Clause-②: no ## What changed `os package publish` now sends `visibility` in the package upsert only when `--visibility` was passed. - `packages/cli/src/commands/package/publish.ts` - The `--visibility` flag loses its `default: 'org'`. An omitted flag is an omitted key in the `POST /api/v1/cloud/packages` body. - The summary's `Visibility` line reads the control plane's answer (`pkg.visibility`), else the value this publish explicitly asked for. With neither, it prints `not reported by the control plane`. It never prints `undefined`, and it never reads a local default. - The "visibility is marketplace but the version is still draft" hint reads that same value, not the flag. It now also fires when a `marketplace` package is re-published without the flag. - Help text: the flag says what omitting it does, and `--submit` states its precondition as the package's visibility, not the flag. - `packages/cli/test/package-publish-visibility.test.ts`: the pins (below). - `content/docs/deployment/cli.mdx` and `publish-and-preview.mdx`: the `--visibility` rows drop "default `org`" and say the control plane decides when the flag is omitted. The `--submit` row in `cli.mdx` is also updated; see Acceptance notes ③. - `.changeset/20892-publish-visibility-when-asked.md`: `@objectstack/cli` patch. **Consumer seam, not changed here:** objectstack-ai/cloud `POST /api/v1/cloud/packages` (`packages/service-cloud/src/routes/package-publish.ts`). For an existing package it sets visibility whenever the body carries one. For a new package it applies an absent value as `org` (cloud ADR-0007 decision ②, as the `repo:cloud` seat reads it). This PR relies on both behaviours and edits neither. **Scope held to the producer half.** The spec's `PackageSchema.visibility.default('private')` is not touched. It conflicts with ADR-0006 v4 open question 2 and with the shipped `org`, and that question stays with #20900, which remains open. A new package created without the flag still gets `org` from the route, as before, so this PR decides nothing there. ## Repro (before) and the same probe after This uses the real CLI source entry, `packages/cli/bin/run-dev.js package publish ARTIFACT --server http://127.0.0.1:PORT --token T`. The target is a local capture server that applies the upsert semantics the card measured, with the package pre-stored as `marketplace`. | | upsert body captured | stored after | |---|---|---| | before (`33b6e8bece`), no flag | `{"manifest_id":"com.acme.visprobe","display_name":"Vis Probe","visibility":"org"}` | `org` | | before, `--submit` | same body, then `submit_for_review: true` | `org` | | after (`4ecc5a3cd6`), no flag | `{"manifest_id":"com.acme.visprobe","display_name":"Vis Probe"}` | `marketplace` | | after, `--submit` | no `visibility`, then `submit_for_review: true` | `marketplace` | | after, `--visibility org` | `...,"visibility":"org"}` | `org` | The "before" body is byte-identical to the one the cloud seat captured on the card. ## Pins (`test/package-publish-visibility.test.ts`, unit tier) 1. The upsert body has no `visibility` key when the flag is absent. 2. The flag value is carried for each of `private` / `org` / `marketplace` (and `CreatePackageRequestSchema.visibility` accepts it). 3. Re-publishing a stored `marketplace` package without the flag leaves it `marketplace`. The summary prints `marketplace` and the draft hint fires. 4. Control: the same re-publish with `--visibility org` moves the package to `org`. This shows the stand-in can demote, so pin 3 is not vacuous. 5. Creating without the flag yields the route's default (`org`), and the summary prints the route's answer. 6. When neither the flag nor the answer names a visibility, the summary says `not reported by the control plane`, never `undefined`. 7. `--submit` without the flag sends `submit_for_review: true`, sends no `visibility`, and is not refused client-side. There is no client-side precondition on the flag: PM mechanism assumption 3 measured false, and the server judges the stored value. The stand-in route models only the two route facts the card's cloud seat read and measured. Every pass/fail assertion is about what the CLI sent or printed. ## Verification (all at `4ecc5a3cd6`) - **Ablation.** The fix was committed first. Each mutation went through `scripts/ablation-replace.mjs` in WRAP mode, with a trap-armed restore. The subject is imported from `src/` by a relative path, so no `dist/` leg applies. - A: body always carries `flags.visibility ?? 'org'`. 4 red (pins 1, 3, 5, 7), 5 green. - B: summary prints `String(visibility)`. 1 red (pin 6). - C: hint reads `flags.visibility`. 1 red (pin 3). - Each restore was proven: blob == HEAD `4f6c58ac18df` and `git diff HEAD` empty. - **Tests.** `pnpm --filter @objectstack/cli exec vitest run --project unit --maxWorkers=2`: 239 files, 3391 tests passed. The six publish-related files alone: 63 of 63. The `integration` tier is declared to CI, since this diff touches no integration-tier file or spawn entry. - **Typecheck.** `pnpm --filter @objectstack/cli typecheck` exit 0. The new test file is in the `tsconfig.test.json` program (`--listFiles`: 1 hit) and adds 0 errors; the 28 ledgered errors in 3 files are unchanged. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands`: 93 derived, 93 run, 0 NOT MEASURED (`--ran` verdict ✓). Three first exited 3 (PREREQUISITE NOT MET: `check:skill-examples`, `check:dual-build-cjs-loads`, `check:i18n-coverage`). After building their prerequisites, all three re-ran with exit 0. - **Lint.** `pnpm lint` (the full `eslint . --no-inline-config`) exit 0, no output. ## Acceptance notes ① `os plugin publish` (`packages/cli/src/commands/plugin/publish.ts`) has the same shape. Its `--visibility` defaults to `private` and is always sent to the same upsert. The real CLI with no flag captured `{"manifest_id":"com.acme.visprobe","display_name":"Vis Probe","visibility":"private"}`, so re-publishing a `marketplace` plugin without the flag would move it to `private`. That file is outside this claim's file surface, so it is reported to the seat and not changed here. ② The upsert answer's shape is not readable from this repository: the cloud repo is not reachable from this session. Whether the route echoes `visibility` is therefore unmeasured. If it does not, the summary falls back to the explicitly requested value, or prints `not reported by the control plane`. ③ `cli.mdx`'s `--submit` row said "Needs `--visibility marketplace`". This change makes that false: a stored `marketplace` package can now be submitted without the flag. The row now names the package's visibility. This goes one row past the dispatch's "only the `--visibility` rows" and is declared as a deviation. ④ `content/docs/releases/v9.mdx` says the flag "defaults to `org`". Its outcome (new packages get `org`) still holds through the route's default. The page is release-owned and is not edited here. Carrier: none. --- _Generated by [Claude Code](https://claude.ai/code/session_01VvcEokUG1tvVxkceYfR5XB)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 975b248 commit def279a

5 files changed

Lines changed: 272 additions & 10 deletions

File tree

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
'@objectstack/cli': patch
3+
---
4+
5+
fix(cli): `os package publish` sends `visibility` only when `--visibility` is passed, so re-publishing no longer moves a `marketplace` package to `org`
6+
7+
Clause-②: no
8+
9+
The `--visibility` flag defaulted to `org`, and the CLI always sent it in the package
10+
upsert (`POST /api/v1/cloud/packages`). That upsert is also the re-publish path, and
11+
the control plane updates an existing package's visibility whenever the request carries
12+
one. So re-publishing a new version of a `marketplace` package without repeating
13+
`--visibility marketplace` silently set it to `org` and took it out of the marketplace.
14+
15+
The flag no longer has a default, and an omitted flag is an omitted key:
16+
17+
- **Re-publish without the flag:** the package keeps its current visibility.
18+
- **First publish without the flag:** the control plane applies its own default
19+
(`org` on ObjectStack Cloud), so a new package gets the same visibility as before.
20+
- **With the flag:** unchanged. `--visibility private|org|marketplace` is sent and
21+
applied, as before.
22+
23+
The publish summary's `Visibility` line shows the control plane's answer. When the
24+
control plane does not report it and no flag was given, the line says
25+
`not reported by the control plane`. The "visibility is marketplace but the version is
26+
still draft" hint now follows that answer too, so it also fires when a `marketplace`
27+
package is re-published without the flag. The `--submit` help text now states its
28+
precondition as the package's visibility being `marketplace` (already stored, or set
29+
with `--visibility marketplace`), since the flag is no longer sent on every publish.

‎content/docs/deployment/cli.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2061,13 +2061,13 @@ when both files' `url`s agree.
20612061
| `--display-name <name>` | — | Name shown in the Marketplace (default `artifact.manifest.name`) |
20622062
| `--description <text>` | — | Short package description |
20632063
| `--category <slug>` | — | Marketplace category slug (`crm`, `hr`, `devtools`, …) |
2064-
| `--visibility <level>` | — | `org` (default, installable across your organization) · `private` (explicit grants only) · `marketplace` (public after review) |
2064+
| `--visibility <level>` | — | `org` (installable across your organization) · `private` (explicit grants only) · `marketplace` (public after review). Omitted: not sent, so the control plane decides — an existing package keeps its visibility, a new one gets the control plane's default (`org` on ObjectStack Cloud) |
20652065
| `--org <id>` | `OS_ORG_ID` | `owner_org_id`. Required with a bearer key in service mode; ignored in user mode |
20662066
| `--env <id>` | `OS_ENVIRONMENT_ID` | Environment to install the new version into. Defaults to the environment [`os environments switch`](#cloud-environments) — or `os environments create --activate` — recorded **for this control plane** in `~/.objectstack/cloud.json` |
20672067
| `--install` | — | Auto-install into `--env` after publishing. With no `--env`, no `$OS_ENVIRONMENT_ID` and no active environment for this control plane, it reports that and publishes without installing |
20682068
| `--seed-sample-data` | — | Include sample data in that auto-install |
20692069
| `--pre-release` | — | Mark the version as a pre-release (also inferred — see below) |
2070-
| `--submit` | — | Submit the new version for marketplace review. Needs `--visibility marketplace` and a complete listing |
2070+
| `--submit` | — | Submit the new version for marketplace review. Needs the package's visibility to be `marketplace` (already stored, or set with `--visibility marketplace`) and a complete listing |
20712071
| `--auto-approve` | — | Platform admin only: skip the review queue and publish straight to the public catalog |
20722072
| `--readme <markdown>` | — | Inline marketplace README. Mutually exclusive with `--readme-file` |
20732073
| `--readme-file <path>` | — | README file, read at publish time. Mutually exclusive with `--readme` |

‎content/docs/deployment/publish-and-preview.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Common flags:
165165
| `--server`, `-s` | `OS_CLOUD_URL` | Cloud control-plane URL |
166166
| `--token`, `-t` | `OS_CLOUD_API_KEY` | Bearer token (service mode) |
167167
| `--version`, `-v` | — | Semver version (default: `artifact.manifest.version`) |
168-
| `--visibility` | — | `org` (default) · `private` · `marketplace` |
168+
| `--visibility` | — | `org` · `private` · `marketplace`. Omitted: the control plane decides — an existing package keeps its visibility, a new one gets its default (`org` on ObjectStack Cloud) |
169169
| `--org` | `OS_ORG_ID` | Owner org id (service mode) |
170170
| `--env` | `OS_ENVIRONMENT_ID` | Environment to install the new version into. Defaults to the environment `os environments switch` (or `os environments create --activate`) recorded for this control plane in `~/.objectstack/cloud.json` |
171171
| `--install` | — | Auto-install the new version into `--env` after publishing |

‎packages/cli/src/commands/package/publish.ts‎

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -324,14 +324,21 @@ export default class PackagePublish extends Command {
324324
category: Flags.string({
325325
description: 'Marketplace category slug (e.g. crm, hr, devtools)',
326326
}),
327+
// ⛔ No `default:` here. The package upsert is also the RE-publish path, and
328+
// the control plane patches an existing package's visibility whenever the
329+
// body carries one — it cannot tell a CLI default from a choice. A default
330+
// on this flag therefore rewrote every package on every publish: a
331+
// `marketplace` package re-published without repeating the flag silently
332+
// became `org` and left the marketplace. Omitted flag ⇒ omitted key.
327333
visibility: Flags.string({
328334
description:
329335
"Who can see / install this package. " +
330-
"'org' (default): auto-visible/installable across your organization's environments. " +
336+
"'org': auto-visible/installable across your organization's environments. " +
331337
"'private': only explicitly-granted orgs/envs. " +
332-
"'marketplace': public after review.",
338+
"'marketplace': public after review. " +
339+
'Omit it to leave an existing package\'s visibility unchanged; a new package then gets ' +
340+
"the control plane's default ('org' on ObjectStack Cloud).",
333341
options: ['private', 'org', 'marketplace'],
334-
default: 'org',
335342
}),
336343
org: Flags.string({
337344
description: 'owner_org_id (required when using a bearer key in service mode; ignored in user mode)',
@@ -358,7 +365,8 @@ export default class PackagePublish extends Command {
358365
submit: Flags.boolean({
359366
description:
360367
'After publishing, submit the new version for marketplace review ' +
361-
'(requires --visibility=marketplace and a complete listing).',
368+
"(requires the package's visibility to be marketplace — already stored, or set with " +
369+
'--visibility marketplace — and a complete listing).',
362370
default: false,
363371
}),
364372
'auto-approve': Flags.boolean({
@@ -521,8 +529,11 @@ export default class PackagePublish extends Command {
521529
const pkgBody: Record<string, any> = {
522530
manifest_id: manifestId,
523531
display_name: displayName,
524-
visibility: flags.visibility,
525532
};
533+
// Absent flag ⇒ absent key, so the control plane keeps an existing
534+
// package's visibility and applies its own default to a new one.
535+
// `CreatePackageRequestSchema.visibility` is optional; see the flag.
536+
if (flags.visibility) pkgBody.visibility = flags.visibility;
526537
// Absent namespace ⇒ absent key. `CreatePackageRequestSchema.namespace`
527538
// is optional and §A.2 allows an unnamespaced publish; sending `null` or
528539
// `''` would turn "declares no namespace" into a value the gate has to
@@ -614,6 +625,12 @@ export default class PackagePublish extends Command {
614625
}
615626
const pkg = pkgRes.body?.data ?? pkgRes.body;
616627
printSuccess(`${pkg?.created ? 'Created' : 'Updated'} sys_package ${pkg?.id} (${manifestId})`);
628+
// The visibility the package now has: the control plane's answer, else
629+
// the value this publish explicitly asked for (the upsert stores it).
630+
// With the flag omitted only the control plane knows — there is no
631+
// local default to fall back on, and the summary says so.
632+
const visibility: string | undefined =
633+
typeof pkg?.visibility === 'string' ? pkg.visibility : flags.visibility;
617634

618635
// ---- Step 1b: optional icon upload ---------------------------------
619636
// When --icon-file is set we upload raw bytes BEFORE version publish.
@@ -735,7 +752,7 @@ export default class PackagePublish extends Command {
735752
printKV(' Version', String(ver?.version ?? version));
736753
printKV(' Version ID', String(ver?.id ?? '—'));
737754
if (ver?.checksum) printKV(' Checksum', String(ver.checksum).slice(0, 16));
738-
printKV(' Visibility', String(pkg?.visibility ?? flags.visibility));
755+
printKV(' Visibility', visibility ?? 'not reported by the control plane');
739756
if (pkg?.owner_org_id) printKV(' Owner Org', String(pkg.owner_org_id));
740757
if (ver?.installation) {
741758
console.log('');
@@ -751,7 +768,7 @@ export default class PackagePublish extends Command {
751768
if (ver?.listing_status) {
752769
console.log('');
753770
printKV(' Listing', String(ver.listing_status));
754-
if (ver.listing_status === 'draft' && flags.visibility === 'marketplace') {
771+
if (ver.listing_status === 'draft' && visibility === 'marketplace') {
755772
console.log('');
756773
console.log(
757774
' Hint: visibility is marketplace but the version is still draft. Pass --submit on the\n' +
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* `os package publish` sends `visibility` only when the author passed
5+
* `--visibility` (#20892).
6+
*
7+
* The package upsert (`POST /api/v1/cloud/packages`) is also the RE-publish
8+
* path, and for an existing package the control plane patches visibility
9+
* whenever the body carries one. The flag used to default to `'org'` and was
10+
* always sent, so re-publishing a new version of a `marketplace` package
11+
* without repeating `--visibility marketplace` silently moved it to `org` and
12+
* out of the marketplace. The route cannot tell a CLI default from a choice,
13+
* so the fix lives at the producer: an omitted flag is an omitted key.
14+
*
15+
* ## The stand-in route, and what it is NOT
16+
*
17+
* The consumer is objectstack-ai/cloud's route
18+
* (`packages/service-cloud/src/routes/package-publish.ts`), which this
19+
* repository cannot run. `standInRoute()` below models only the two facts the
20+
* card's cloud seat read and measured on that route — for an EXISTING package
21+
* `if (body.visibility) patch.visibility = visibility`, and for a NEW package
22+
* an absent value defaults to `org` (cloud ADR-0007 decision 2) — so the
23+
* "re-publish leaves it unchanged" and "create yields the route's default"
24+
* cases can be stated end to end. Every assertion that decides pass or fail
25+
* here is about what the CLI SENT or PRINTED; the stand-in's stored value is
26+
* the consequence, and the control case (`--visibility org` DOES change it)
27+
* proves the stand-in is capable of the demotion the pins say no longer
28+
* happens.
29+
*/
30+
31+
import { describe, it, expect, afterEach, beforeEach, vi } from 'vitest';
32+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
33+
import { tmpdir } from 'node:os';
34+
import { join } from 'node:path';
35+
import { CreatePackageRequestSchema } from '@objectstack/spec/marketplace';
36+
import PackagePublish from '../src/commands/package/publish.js';
37+
38+
type Call = { url: string; body: Record<string, unknown> };
39+
type StoredPackage = { id: string; visibility: string };
40+
41+
const MANIFEST_ID = 'com.acme.crm';
42+
43+
/**
44+
* Stub `fetch` as the stand-in route described in the header. `rows` is the
45+
* control plane's package table, keyed by manifest id; pass a row to model a
46+
* re-publish, omit it to model a first publish. `echoVisibility: false` models
47+
* a control plane whose upsert answer does not report the stored value.
48+
*/
49+
function standInRoute(
50+
rows: Map<string, StoredPackage>,
51+
opts: { echoVisibility?: boolean } = {},
52+
): Call[] {
53+
const calls: Call[] = [];
54+
vi.stubGlobal('fetch', vi.fn(async (url: string, init: { body: string }) => {
55+
const body = JSON.parse(init.body) as Record<string, unknown>;
56+
calls.push({ url, body });
57+
let data: Record<string, unknown>;
58+
if (url.endsWith('/versions')) {
59+
data = { id: 'ver_1', version: '1.2.0', listing_status: 'draft' };
60+
} else {
61+
const manifestId = String(body.manifest_id);
62+
const existing = rows.get(manifestId);
63+
let row: StoredPackage;
64+
if (existing) {
65+
if (body.visibility) existing.visibility = String(body.visibility);
66+
row = existing;
67+
} else {
68+
row = { id: 'pkg_new', visibility: typeof body.visibility === 'string' ? body.visibility : 'org' };
69+
rows.set(manifestId, row);
70+
}
71+
data = { id: row.id, created: !existing };
72+
if (opts.echoVisibility !== false) data.visibility = row.visibility;
73+
}
74+
return { ok: true, status: 200, statusText: 'OK', json: async () => ({ success: true, data }) } as unknown as Response;
75+
}));
76+
return calls;
77+
}
78+
79+
describe('os package publish — `visibility` is sent only when asked for', () => {
80+
let dir = '';
81+
const prevEnv = { url: process.env.OS_CLOUD_URL, key: process.env.OS_CLOUD_API_KEY };
82+
const prevCwd = process.cwd();
83+
let output: string[] = [];
84+
85+
beforeEach(() => {
86+
output = [];
87+
for (const channel of ['error', 'log', 'warn'] as const) {
88+
vi.spyOn(console, channel).mockImplementation((...args: unknown[]) => {
89+
// Strip ANSI styling so the assertions read the words, not the colours.
90+
output.push(args.map(String).join(' ').replace(/\u001b\[[0-9;]*m/g, ''));
91+
});
92+
}
93+
});
94+
95+
afterEach(async () => {
96+
process.chdir(prevCwd);
97+
vi.unstubAllGlobals();
98+
vi.restoreAllMocks();
99+
process.env.OS_CLOUD_URL = prevEnv.url;
100+
process.env.OS_CLOUD_API_KEY = prevEnv.key;
101+
if (dir) await rm(dir, { recursive: true, force: true });
102+
dir = '';
103+
});
104+
105+
async function artifact(): Promise<string> {
106+
dir = await mkdtemp(join(tmpdir(), 'package-publish-visibility-'));
107+
const path = join(dir, 'objectstack.json');
108+
await writeFile(
109+
path,
110+
JSON.stringify({ manifest: { id: MANIFEST_ID, name: 'Acme CRM', version: '1.2.0' }, objects: [] }),
111+
);
112+
process.env.OS_CLOUD_URL = 'http://cloud.test';
113+
process.env.OS_CLOUD_API_KEY = 'tok_123';
114+
return path;
115+
}
116+
117+
function printed(label: string): string | undefined {
118+
const line = output.find((l) => l.trimStart().startsWith(`${label}:`));
119+
return line?.slice(line.indexOf(':') + 1).trim();
120+
}
121+
122+
it('omits the `visibility` key from the upsert body when --visibility is not passed', async () => {
123+
const path = await artifact();
124+
const calls = standInRoute(new Map());
125+
126+
await PackagePublish.run([path]);
127+
128+
expect(calls).toHaveLength(2);
129+
expect(calls[0].url).toBe('http://cloud.test/api/v1/cloud/packages');
130+
expect('visibility' in calls[0].body).toBe(false);
131+
// The acceptance face treats an absent visibility as legal.
132+
expect(CreatePackageRequestSchema.shape.visibility.safeParse(undefined).success).toBe(true);
133+
});
134+
135+
it.each(['private', 'org', 'marketplace'] as const)(
136+
'carries the flag value when --visibility %s is passed',
137+
async (value) => {
138+
const path = await artifact();
139+
const calls = standInRoute(new Map());
140+
141+
await PackagePublish.run([path, '--visibility', value]);
142+
143+
expect(calls[0].body.visibility).toBe(value);
144+
expect(CreatePackageRequestSchema.shape.visibility.safeParse(value).success).toBe(true);
145+
},
146+
);
147+
148+
it('re-publishing a `marketplace` package without the flag leaves its visibility unchanged', async () => {
149+
const path = await artifact();
150+
const rows = new Map([[MANIFEST_ID, { id: 'pkg_1', visibility: 'marketplace' }]]);
151+
const calls = standInRoute(rows);
152+
153+
await PackagePublish.run([path]);
154+
155+
expect('visibility' in calls[0].body).toBe(false);
156+
expect(rows.get(MANIFEST_ID)?.visibility).toBe('marketplace');
157+
// The summary reports the control plane's answer, not a local default…
158+
expect(printed('Visibility')).toBe('marketplace');
159+
// …and the draft-listing hint fires on that answer too.
160+
expect(output.join('\n')).toContain('Hint: visibility is marketplace but the version is still draft');
161+
});
162+
163+
// Control: the stand-in DOES demote when the body asks it to, so the case
164+
// above is green because the CLI stopped asking, not because nothing could
165+
// have changed the row.
166+
it('control — an explicit --visibility org on the same re-publish does move it', async () => {
167+
const path = await artifact();
168+
const rows = new Map([[MANIFEST_ID, { id: 'pkg_1', visibility: 'marketplace' }]]);
169+
standInRoute(rows);
170+
171+
await PackagePublish.run([path, '--visibility', 'org']);
172+
173+
expect(rows.get(MANIFEST_ID)?.visibility).toBe('org');
174+
expect(printed('Visibility')).toBe('org');
175+
expect(output.join('\n')).not.toContain('Hint: visibility is marketplace');
176+
});
177+
178+
it('creating a package without the flag yields what the route decides, and prints that', async () => {
179+
const path = await artifact();
180+
const rows = new Map<string, StoredPackage>();
181+
const calls = standInRoute(rows);
182+
183+
await PackagePublish.run([path]);
184+
185+
expect('visibility' in calls[0].body).toBe(false);
186+
expect(rows.get(MANIFEST_ID)?.visibility).toBe('org');
187+
expect(printed('Visibility')).toBe('org');
188+
});
189+
190+
it('says so when neither the flag nor the control plane names the visibility — never `undefined`', async () => {
191+
const path = await artifact();
192+
standInRoute(new Map([[MANIFEST_ID, { id: 'pkg_1', visibility: 'marketplace' }]]), { echoVisibility: false });
193+
194+
await PackagePublish.run([path]);
195+
196+
expect(printed('Visibility')).toBe('not reported by the control plane');
197+
expect(output.join('\n')).not.toMatch(/Visibility:\s*undefined/);
198+
});
199+
200+
// `--submit` has no client-side precondition on the flag; the control plane
201+
// judges the STORED visibility. With the default gone, an omitted flag must
202+
// neither be refused locally nor re-introduce a visibility on the wire.
203+
it('--submit without --visibility submits for review and sends no visibility', async () => {
204+
const path = await artifact();
205+
const rows = new Map([[MANIFEST_ID, { id: 'pkg_1', visibility: 'marketplace' }]]);
206+
const calls = standInRoute(rows);
207+
208+
await PackagePublish.run([path, '--submit']);
209+
210+
expect(calls).toHaveLength(2);
211+
expect('visibility' in calls[0].body).toBe(false);
212+
expect(calls[1].url).toBe('http://cloud.test/api/v1/cloud/packages/pkg_1/versions');
213+
expect(calls[1].body.submit_for_review).toBe(true);
214+
expect(rows.get(MANIFEST_ID)?.visibility).toBe('marketplace');
215+
});
216+
});

0 commit comments

Comments
 (0)