@bongos/core 1.19.209 → 1.19.211

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,168 @@
1
+ // tests/github_repo_visibility.mjs — the manage page's repo-visibility routes
2
+ // (task 1003045): GET /github/repo + POST /github/repo/visibility.
3
+ //
4
+ // The load-bearing assertions:
5
+ // • the PRIVATE direction is refused BEFORE GitHub is ever called (409
6
+ // private_not_deployable): the deploy pull runs as the shared deploy key,
7
+ // which cannot read a private repo (walk finding N4), so flipping private
8
+ // would break the project's next deploy — the honest-refusal leg this task
9
+ // ships. The refusal must not even consult the stored token.
10
+ // • flip-to-public calls makeRepoPublic with the parsed owner/name and
11
+ // forwards GitHub's own answer — the new state is never assumed
12
+ // • a GitHub 401 drops the stored token and re-prompts (the /github/repos
13
+ // convention); GitHub's 404 forwards as repo_not_found (no leak beyond what
14
+ // GitHub told this account); a GitHub 403 becomes not_repo_admin
15
+ // • a malformed owner/name is a clean 400 bad_repo_ref
16
+ //
17
+ // DB-free + network-free: db.getGithubToken and the github-repos helpers are
18
+ // stubbed on their cached module objects (the github_token_store.mjs pattern —
19
+ // which is also why routes/github.js requires the helpers as a namespace).
20
+
21
+ import { strict as assert } from 'node:assert';
22
+ import { test } from 'node:test';
23
+ import { createRequire } from 'node:module';
24
+ import path from 'node:path';
25
+ import { fileURLToPath } from 'node:url';
26
+
27
+ // A valid key BEFORE requiring the modules, so secret-box reads as configured.
28
+ process.env.BUILDER_SECRET_KEY = 'ab'.repeat(32);
29
+
30
+ const require = createRequire(import.meta.url);
31
+ const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
32
+ const db = require(path.join(ROOT, 'src/bongos/db.js'));
33
+ const githubRepos = require(path.join(ROOT, 'src/bongos/github-repos.js'));
34
+ const buildGithubRouter = require(path.join(ROOT, 'src/bongos/routes/github.js'));
35
+ const { attachFail } = require(path.join(ROOT, 'src/bongos/middleware/error-envelope.js'));
36
+
37
+ const { GithubApiError } = githubRepos;
38
+
39
+ function fakeRes() {
40
+ const res = { _status: 200, _body: null };
41
+ res.status = (c) => { res._status = c; return res; };
42
+ res.json = (b) => { res._body = b; return res; };
43
+ attachFail({}, res, () => {}); // installs res.fail exactly as the router does
44
+ return res;
45
+ }
46
+ function finalHandler(router, method, pth) {
47
+ const layer = router.stack.find((l) => l.route && l.route.path === pth && l.route.methods[method]);
48
+ const stack = layer.route.stack;
49
+ return stack[stack.length - 1].handle; // the handler after requireBuilder
50
+ }
51
+
52
+ // One stubbed world per test: a live repo-scoped token + programmable helpers.
53
+ // getRepo/makePublic are thunks so a test can return an answer or throw a typed
54
+ // GithubApiError, exactly as the real helpers do.
55
+ function stub({ token = { token: 'tok', scope: 'repo read:user' }, getRepo, makePublic } = {}) {
56
+ const orig = {
57
+ get: db.getGithubToken, del: db.deleteGithubToken,
58
+ read: githubRepos.getUserRepo, flip: githubRepos.makeRepoPublic,
59
+ };
60
+ const calls = { read: [], flip: [], del: 0 };
61
+ db.getGithubToken = async () => token;
62
+ db.deleteGithubToken = async () => { calls.del++; };
63
+ githubRepos.getUserRepo = async (_t, ref) => { calls.read.push(ref); return getRepo(); };
64
+ githubRepos.makeRepoPublic = async (_t, ref) => { calls.flip.push(ref); return makePublic(); };
65
+ const restore = () => {
66
+ db.getGithubToken = orig.get; db.deleteGithubToken = orig.del;
67
+ githubRepos.getUserRepo = orig.read; githubRepos.makeRepoPublic = orig.flip;
68
+ };
69
+ return { calls, restore };
70
+ }
71
+
72
+ const getHandler = () => finalHandler(buildGithubRouter(), 'get', '/github/repo');
73
+ const postHandler = () => finalHandler(buildGithubRouter(), 'post', '/github/repo/visibility');
74
+
75
+ test('GET /github/repo: forwards GitHub\'s answer for the caller\'s own credential', async () => {
76
+ const s = stub({ getRepo: () => ({ full_name: 'jaxri/mercury', html_url: 'https://github.com/jaxri/mercury', private: true, admin: true }) });
77
+ try {
78
+ const res = fakeRes();
79
+ await getHandler()({ builder: { id: 1 }, query: { repo: 'jaxri/mercury' } }, res);
80
+ assert.equal(res._status, 200);
81
+ assert.equal(res._body.repo.private, true);
82
+ assert.equal(res._body.repo.admin, true);
83
+ assert.equal(res._body.private_deployable, false, 'the platform truth rides every answer');
84
+ assert.deepEqual(s.calls.read, [{ owner: 'jaxri', name: 'mercury' }]);
85
+ } finally { s.restore(); }
86
+ });
87
+
88
+ test('GET /github/repo: a malformed ref is a clean 400 before any token or GitHub touch', async () => {
89
+ const s = stub({ getRepo: () => ({}) });
90
+ try {
91
+ for (const bad of ['', 'no-slash', '-lead/x', 'a/b/c', 'owner/', 'javascript:alert(1)']) {
92
+ const res = fakeRes();
93
+ await getHandler()({ builder: { id: 1 }, query: { repo: bad } }, res);
94
+ assert.equal(res._status, 400, `ref ${JSON.stringify(bad)} must 400`);
95
+ assert.equal(res._body.error.code, 'bad_repo_ref');
96
+ }
97
+ assert.equal(s.calls.read.length, 0, 'GitHub is never consulted for a malformed ref');
98
+ } finally { s.restore(); }
99
+ });
100
+
101
+ test('GET /github/repo: GitHub\'s 404 forwards as repo_not_found — no leak beyond GitHub\'s own answer', async () => {
102
+ const s = stub({ getRepo: () => { throw new GithubApiError('nope', { status: 404, code: 'repo_not_found' }); } });
103
+ try {
104
+ const res = fakeRes();
105
+ await getHandler()({ builder: { id: 1 }, query: { repo: 'jaxri/gone' } }, res);
106
+ assert.equal(res._status, 404);
107
+ assert.equal(res._body.error.code, 'repo_not_found');
108
+ } finally { s.restore(); }
109
+ });
110
+
111
+ test('GET /github/repo: a GitHub 401 drops the stored token and re-prompts (the /github/repos convention)', async () => {
112
+ const s = stub({ getRepo: () => { throw new GithubApiError('bad', { status: 401, code: 'github_unauthorized' }); } });
113
+ try {
114
+ const res = fakeRes();
115
+ await getHandler()({ builder: { id: 1 }, query: { repo: 'jaxri/mercury' } }, res);
116
+ assert.equal(res._status, 403);
117
+ assert.equal(res._body.error.code, 'repo_scope_required');
118
+ assert.equal(s.calls.del, 1, 'the dead token is swept so the next attempt re-prompts cleanly');
119
+ } finally { s.restore(); }
120
+ });
121
+
122
+ test('POST visibility=private: refused 409 private_not_deployable — GitHub and the token store untouched', async () => {
123
+ const s = stub({ makePublic: () => ({}) });
124
+ const origGet = db.getGithubToken;
125
+ let tokenReads = 0;
126
+ db.getGithubToken = async () => { tokenReads++; return { token: 'tok', scope: 'repo' }; };
127
+ try {
128
+ const res = fakeRes();
129
+ await postHandler()({ builder: { id: 1 }, body: { repo: 'jaxri/mercury', visibility: 'private' } }, res);
130
+ assert.equal(res._status, 409);
131
+ assert.equal(res._body.error.code, 'private_not_deployable');
132
+ assert.match(res._body.error.message, /deploy/i, 'the refusal says WHY in plain words');
133
+ assert.equal(s.calls.flip.length, 0, 'no GitHub call for a refused direction');
134
+ assert.equal(tokenReads, 0, 'the refusal happens before the credential is even read');
135
+ } finally { db.getGithubToken = origGet; s.restore(); }
136
+ });
137
+
138
+ test('POST visibility=public: flips via makeRepoPublic and forwards GitHub\'s answer', async () => {
139
+ const s = stub({ makePublic: () => ({ full_name: 'jaxri/mercury', html_url: 'https://github.com/jaxri/mercury', private: false, admin: true }) });
140
+ try {
141
+ const res = fakeRes();
142
+ await postHandler()({ builder: { id: 1 }, body: { repo: 'jaxri/mercury', visibility: 'public' } }, res);
143
+ assert.equal(res._status, 200);
144
+ assert.equal(res._body.ok, true);
145
+ assert.equal(res._body.repo.private, false, 'the new state is GitHub\'s answer, never assumed');
146
+ assert.deepEqual(s.calls.flip, [{ owner: 'jaxri', name: 'mercury' }]);
147
+ } finally { s.restore(); }
148
+ });
149
+
150
+ test('POST visibility=public: GitHub\'s 403 becomes not_repo_admin, said plainly', async () => {
151
+ const s = stub({ makePublic: () => { throw new GithubApiError('no', { status: 403, code: 'not_repo_admin' }); } });
152
+ try {
153
+ const res = fakeRes();
154
+ await postHandler()({ builder: { id: 1 }, body: { repo: 'other/repo', visibility: 'public' } }, res);
155
+ assert.equal(res._status, 403);
156
+ assert.equal(res._body.error.code, 'not_repo_admin');
157
+ } finally { s.restore(); }
158
+ });
159
+
160
+ test('POST: an off-vocabulary visibility is a 400 (strict enum), never silently coerced', async () => {
161
+ const s = stub({ makePublic: () => ({}) });
162
+ try {
163
+ const res = fakeRes();
164
+ await postHandler()({ builder: { id: 1 }, body: { repo: 'jaxri/mercury', visibility: 'internal' } }, res);
165
+ assert.equal(res._status, 400);
166
+ assert.equal(s.calls.flip.length, 0);
167
+ } finally { s.restore(); }
168
+ });
@@ -114,3 +114,52 @@ test('createUserRepo: 422 → typed name_taken (the route maps this to a friendl
114
114
  (e) => e.code === 'name_taken' && e.status === 422,
115
115
  );
116
116
  });
117
+
118
+ // ---- repo-visibility helpers (task 1003045) ----
119
+
120
+ test('getUserRepo: asks /repos/{owner}/{name}, maps private + admin off GitHub\'s answer', async () => {
121
+ let calledUrl = '';
122
+ const fetchImpl = async (url) => {
123
+ calledUrl = url;
124
+ return res(200, {
125
+ full_name: 'me/mercury', html_url: 'https://github.com/me/mercury',
126
+ private: true, permissions: { admin: true, push: true },
127
+ });
128
+ };
129
+ const out = await gh.getUserRepo('tok', { owner: 'me', name: 'mercury', fetchImpl });
130
+ assert.match(calledUrl, /\/repos\/me\/mercury$/);
131
+ assert.deepEqual(out, { full_name: 'me/mercury', html_url: 'https://github.com/me/mercury', private: true, admin: true });
132
+ });
133
+
134
+ test('getUserRepo: 404 → typed repo_not_found (GitHub\'s own no-leak shape, forwarded)', async () => {
135
+ const fetchImpl = async () => res(404, { message: 'Not Found' });
136
+ await assert.rejects(
137
+ () => gh.getUserRepo('tok', { owner: 'me', name: 'gone', fetchImpl }),
138
+ (e) => e.code === 'repo_not_found' && e.status === 404,
139
+ );
140
+ });
141
+
142
+ test('makeRepoPublic: PATCHes exactly {private:false} — the private direction is structurally impossible', async () => {
143
+ // Like createUserRepo's private:false, the direction is deliberately not a
144
+ // parameter: the deploy pull runs as the shared deploy key that cannot read a
145
+ // private repo (N4), so nothing in the codebase may flip a repo private.
146
+ let sentMethod = '';
147
+ let sentBody = null;
148
+ const fetchImpl = async (url, init) => {
149
+ sentMethod = init.method;
150
+ sentBody = JSON.parse(init.body);
151
+ return res(200, { full_name: 'me/mercury', html_url: 'https://github.com/me/mercury', private: false, permissions: { admin: true } });
152
+ };
153
+ const out = await gh.makeRepoPublic('tok', { owner: 'me', name: 'mercury', fetchImpl });
154
+ assert.equal(sentMethod, 'PATCH');
155
+ assert.deepEqual(sentBody, { private: false }, 'only the public direction can ever travel');
156
+ assert.equal(out.private, false);
157
+ });
158
+
159
+ test('makeRepoPublic: 403 → typed not_repo_admin', async () => {
160
+ const fetchImpl = async () => res(403, { message: 'Must have admin rights' });
161
+ await assert.rejects(
162
+ () => gh.makeRepoPublic('tok', { owner: 'other', name: 'repo', fetchImpl }),
163
+ (e) => e.code === 'not_repo_admin' && e.status === 403,
164
+ );
165
+ });
@@ -51,7 +51,11 @@ test('the nav carries Home plus the three views, and Home is not a view', () =>
51
51
 
52
52
  test('?view= is the deep-link contract, read on boot and written with replaceState', () => {
53
53
  assert.match(SCRIPT, /new URLSearchParams\(location\.search\)\.get\('view'\)/);
54
- assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v\)/);
54
+ assert.match(SCRIPT, /history\.replaceState\(null, '', '\?view=' \+ v \+ idPart\)/);
55
+ // the manage view is per-project, so its deep link carries the id too
56
+ // (task 1003041) — and the id is encoded, never raw-interpolated
57
+ assert.match(SCRIPT, /'&id=' \+ encodeURIComponent\(manageId\)/,
58
+ 'the URL writer must carry the manage id, encoded');
55
59
  // the landing page's CTAs have always pointed here — this is the bug they hit
56
60
  const landing = fs.readFileSync(path.join(ROOT, 'public-landing', 'index.html'), 'utf8');
57
61
  assert.match(landing, /href="\/projects\?view=new"/,
@@ -753,7 +757,8 @@ test('owned truth reaches the TABLE too — same list, same chip, same manage jo
753
757
  });
754
758
 
755
759
  test('an owned orb\'s job is manage, not the public site', () => {
756
- assert.match(SCRIPT, /'\?view=mine'/, 'the owned href is the manage deep-link, real and middle-clickable');
760
+ assert.match(SCRIPT, /'\?view=manage&id=' \+ encodeURIComponent\(String\(inst\.id\)\)/,
761
+ 'the owned href is the manage deep-link, real and middle-clickable');
757
762
  assert.match(SCRIPT, /hasAttribute\('data-manage'\)\) return goToManage/,
758
763
  'a plain click on an owned orb routes to manage before the card path');
759
764
  const orbs = (SCRIPT.match(/function renderOrbs\(([\s\S]*?)\n \}/) || [, ''])[1];
@@ -794,13 +799,15 @@ test('the orb chip is the manage list\'s truth, never a second story (C6)', () =
794
799
  'no instance chip may be literal-classed live — chipMarkup() decides from the overlay');
795
800
  });
796
801
 
797
- test('click-to-manage anchors the project and defers until the card exists', () => {
798
- assert.match(SCRIPT, /function goToManage/);
799
- assert.match(SCRIPT, /pendingAnchor/);
800
- assert.match(SCRIPT, /scrollIntoView/);
801
- const fn = (SCRIPT.match(/function tryAnchor\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
802
- assert.ok(fn, 'tryAnchor() must be extractable');
803
- assert.match(fn, /mineClear/, 'a filter that hides the anchored card is cleared, not silently no-op\'d');
802
+ test('click-to-manage opens the per-project manage view (task 1003041)', () => {
803
+ const fn = (SCRIPT.match(/function goToManage\(instId\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
804
+ assert.ok(fn, 'goToManage() must be extractable');
805
+ assert.match(fn, /manageId = String\(instId/, 'the target project rides the module state');
806
+ assert.match(fn, /setView\('manage'\)/, 'the destination is the manage view, not a scroll anchor');
807
+ // the scroll-anchor era ended with its caller — a half-removed pendingAnchor
808
+ // would silently no-op every manage click
809
+ assert.doesNotMatch(SCRIPT, /pendingAnchor/, 'the anchor machinery must be fully gone');
810
+ assert.doesNotMatch(COSMOS, /\.proj\.anchored/, 'and so must its landing-glow rule');
804
811
  });
805
812
 
806
813
  test('the map does not poll for owned truth — it re-reads on entry (C8)', () => {
@@ -887,3 +894,145 @@ test('zoom heads for the cursor and survives line-mode wheels', () => {
887
894
  assert.ok(COSMOS.includes('transform-origin:0 0'),
888
895
  'the camera algebra assumes the layer scales about its origin');
889
896
  });
897
+
898
+ /* ── The manage view (task 1003041, goal-1000058 wave) ──────────────────────
899
+ One owned project's management home: ?view=manage&id=N. The identity block
900
+ renders the own-scoped per-id read (with the verification overlay); the
901
+ settings sections are the homes the sibling manage-wave tasks fill in, each
902
+ an honest not-wired-yet until its task ships. The API is the wall: 404 for
903
+ strangers (owner or archon only), so the page renders one clean not-found
904
+ and never a permission hint. ── */
905
+
906
+ test('the manage view ships as real markup with the four settings homes (C7)', () => {
907
+ assert.match(HUB, /id="view-manage"/);
908
+ for (const id of ['mGate', 'mSignin', 'mMissing', 'mAlert', 'mLoading', 'mMain',
909
+ 'mName', 'mChip', 'mMeta', 'mAddr', 'mCreated',
910
+ 'mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
911
+ assert.match(HUB, new RegExp(`id="${id}"`), `${id} must be real markup`);
912
+ }
913
+ // and the not-yet copy a crawler reads is in the document, not a template string
914
+ assert.match(HUB, /isn’t wired up/, 'the honest empty states live in the markup');
915
+ });
916
+
917
+ test('the manage view balances its own tree, like the wizard', () => {
918
+ const start = HUB.indexOf('<section id="view-manage"');
919
+ const end = HUB.indexOf('<section id="view-new"');
920
+ assert.ok(start !== -1 && end > start, 'need the view-manage span');
921
+ const manage = HUB.slice(start, end);
922
+ assert.equal((manage.match(/<div\b/g) || []).length, (manage.match(/<\/div>/g) || []).length,
923
+ 'every div opened in #view-manage closes in #view-manage');
924
+ assert.equal((manage.match(/<section\b/g) || []).length, (manage.match(/<\/section>/g) || []).length,
925
+ 'every section opened in #view-manage closes in #view-manage');
926
+ });
927
+
928
+ test('?view=manage&id=N is the deep link — read on boot, and no id means the list', () => {
929
+ assert.match(SCRIPT, /if \(v === 'manage'\)/, 'viewFromUrl must know the manage view');
930
+ assert.match(SCRIPT, /\.get\('id'\)/, 'and read the project id off the URL');
931
+ assert.match(SCRIPT, /manageId = String\(id\); return 'manage';/,
932
+ 'an id lands on the manage view');
933
+ assert.match(SCRIPT, /return 'mine';/,
934
+ 'a bare ?view=manage names no project — the list is the honest landing');
935
+ });
936
+
937
+ test('a stranger\'s deep link renders one clean not-found — never a permission hint', () => {
938
+ const missing = (HUB.match(/<section id="mMissing"[\s\S]*?<\/section>/) || [''])[0];
939
+ assert.ok(missing, 'the not-found state must be extractable');
940
+ assert.doesNotMatch(missing, /permission|allowed|access|owner/i,
941
+ 'the 404 copy must not hint that the project exists behind a wall');
942
+ assert.match(SCRIPT, /res\.status === 404[\s\S]{0,220}mMissing/,
943
+ 'a 404 lands on the clean not-found state');
944
+ assert.match(SCRIPT, /res\.status === 401[\s\S]{0,220}mGate/,
945
+ 'a 401 stays a sign-in gate, distinct from not-found');
946
+ });
947
+
948
+ test('the manage chip defers to chipFor — one site, no second story (C6)', () => {
949
+ const fn = (SCRIPT.match(/function renderManage\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
950
+ assert.ok(fn, 'renderManage() must be extractable');
951
+ assert.equal((fn.match(/chipFor\(inst\)/g) || []).length, 1,
952
+ 'renderManage must have exactly one chip call site');
953
+ assert.match(fn, /addressRow\(inst\)/, 'the address sentence is the shared, verified one');
954
+ assert.match(fn, /SHAPE_LABEL/, 'the hosting shape speaks the card\'s vocabulary');
955
+ });
956
+
957
+ test('the manage view spends no timer requests — refresh is entry and tab return (C8)', () => {
958
+ const tick = (SCRIPT.match(/function manageChipTick\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
959
+ assert.ok(tick, 'manageChipTick() must exist');
960
+ assert.match(tick, /currentView !== 'manage'/, 'the tick is view-guarded');
961
+ assert.match(tick, /document\.hidden/, 'and does nothing in a hidden tab');
962
+ assert.doesNotMatch(tick, /fetch\(/, 'zero requests — recency re-renders from data in hand');
963
+ assert.match(SCRIPT, /if \(v === 'manage'\) loadManage\(\);/, 'entry re-reads once');
964
+ assert.match(SCRIPT, /currentView === 'manage' && manageId\) loadManage\(true\)/,
965
+ 'tab return re-reads once, quietly');
966
+ });
967
+
968
+ test('the repo link is built on github.com and escaped — never a raw href', () => {
969
+ assert.match(SCRIPT, /https:\/\/github\.com\/' \+ esc\(inst\.target_ref\)/,
970
+ 'the identity block links the repo through the page escaper on a fixed origin');
971
+ });
972
+
973
+ test('every owned card carries its Manage door, and a modified click keeps its meaning', () => {
974
+ assert.match(SCRIPT, /data-manage="' \+ iid \+ '" href="\?view=manage&id=' \+ iid/,
975
+ 'the card affordance is a real deep-link anchor');
976
+ assert.match(SCRIPT, /closest\('a\[data-manage\]'\)/, 'the delegated handler upgrades it');
977
+ assert.match(SCRIPT, /if \(!isPlainClick\(ev\)\) return;/,
978
+ 'ctrl/middle-click falls through to the browser');
979
+ });
980
+
981
+ /* ── Repo visibility on the manage page (task 1003045) ──────────────────────
982
+ The section reads the repo's REAL visibility from GitHub with the owner's
983
+ own repository credential and offers exactly one direction: private→public.
984
+ The private direction is refused — server-side and here — until the
985
+ private-repo deploy fix lands, because the deploy pull runs as the shared
986
+ deploy key that cannot read a private repo (walk finding N4). ── */
987
+
988
+ test('the repo-visibility section is live markup with its body slot (C7)', () => {
989
+ assert.match(HUB, /id="mRepoVis"/, 'the section body must be real markup');
990
+ assert.match(HUB, /Checking the repository on GitHub…/,
991
+ 'the pre-answer state is honest waiting copy, not a fake answer');
992
+ });
993
+
994
+ test('flip-to-private cannot travel from this page — only the public direction exists', () => {
995
+ assert.match(SCRIPT, /visibility: 'public'/, 'the one POST body pins the public direction');
996
+ assert.doesNotMatch(SCRIPT, /visibility:\s*'private'/,
997
+ 'no code path may send the private direction');
998
+ // the refusal is SAID, with its reason, not silently missing
999
+ assert.match(SCRIPT, /can’t deploy private repositories/,
1000
+ 'the private-repo limitation is stated in plain words');
1001
+ });
1002
+
1003
+ test('the visibility shown is GitHub\'s answer, never an assumption', () => {
1004
+ const fn = (SCRIPT.match(/function doRepoPublic\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1005
+ assert.ok(fn, 'doRepoPublic() must be extractable');
1006
+ assert.match(fn, /manageRepo = data\.repo/,
1007
+ 'after a flip, the rendered state is what GitHub returned');
1008
+ const render = (SCRIPT.match(/function renderRepoVis\(repo\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1009
+ assert.ok(render, 'renderRepoVis() must be extractable');
1010
+ assert.match(render, /repo\.private/, 'the branch reads the live flag');
1011
+ assert.match(render, /repo\.admin/,
1012
+ 'the flip button only shows when GitHub says this account may change settings');
1013
+ });
1014
+
1015
+ test('the GitHub read fires on entry and tab return only — never from the tick\'s render', () => {
1016
+ assert.match(SCRIPT, /loadRepoVisibility\(inst\);/, 'the read rides loadManage\'s success');
1017
+ const render = (SCRIPT.match(/function renderManage\(inst\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1018
+ assert.ok(render, 'renderManage() must be extractable');
1019
+ assert.doesNotMatch(render, /loadRepoVisibility/,
1020
+ 'renderManage is what manageChipTick calls every minute — a read here would poll GitHub (C8)');
1021
+ });
1022
+
1023
+ test('the reconnect door asks for repository access and returns to this project', () => {
1024
+ const fn = (SCRIPT.match(/function repoVisReconnect\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1025
+ assert.ok(fn, 'repoVisReconnect() must be extractable');
1026
+ assert.match(fn, /repos=1/, 'the door carries the repo scope ask');
1027
+ assert.match(fn, /encodeURIComponent\('\/projects\?view=manage&id=' \+ manageId\)/,
1028
+ 'and returns to the exact project being managed');
1029
+ });
1030
+
1031
+ test('making a repo public asks first — exposure is never one accidental click', () => {
1032
+ const fn = (SCRIPT.match(/function askRepoPublic\(\)\s*\{([\s\S]*?)\n \}/) || [, ''])[1];
1033
+ assert.ok(fn, 'askRepoPublic() must be extractable');
1034
+ assert.match(fn, /Anyone on the internet will be able to read the code/,
1035
+ 'the confirm states the consequence in plain words');
1036
+ assert.match(SCRIPT, /data-repo-public-yes/, 'the confirm is a distinct second click');
1037
+ assert.match(SCRIPT, /data-repo-public-no/, 'and declining is offered, not implied');
1038
+ });
@@ -97,7 +97,36 @@ function reset({ inst, as } = {}) {
97
97
  events = [];
98
98
  }
99
99
 
100
- console.log('GET /provisioning/instances/:id/settings:');
100
+ console.log('GET /provisioning/instances/:id (the manage page\'s read — task 1003041):');
101
+
102
+ await ta('owner reads their own instance', async () => {
103
+ reset();
104
+ const r = await get('/provisioning/instances/18');
105
+ assert.equal(r.status, 200, r.raw);
106
+ assert.equal(r.body.instance.slug, 'spike');
107
+ });
108
+
109
+ await ta('ANOTHER builder\'s GET → 404 (no existence leak)', async () => {
110
+ reset({ as: { id: 8, github_login: 'stranger', rank: 'thetes' } });
111
+ const r = await get('/provisioning/instances/18');
112
+ assert.equal(r.status, 404, r.raw);
113
+ assert.equal(r.body.error.code, 'instance_not_found');
114
+ });
115
+
116
+ await ta('an Archon may read ANY instance — the manage-page override (task 1003041)', async () => {
117
+ reset({ as: { id: 999, github_login: 'archon', rank: 'archon' } });
118
+ const r = await get('/provisioning/instances/18');
119
+ assert.equal(r.status, 200, r.raw);
120
+ assert.equal(r.body.instance.slug, 'spike');
121
+ });
122
+
123
+ await ta('unknown instance id → 404 for everyone, archon included', async () => {
124
+ reset({ inst: null, as: { id: 999, github_login: 'archon', rank: 'archon' } });
125
+ const r = await get('/provisioning/instances/999');
126
+ assert.equal(r.status, 404, r.raw);
127
+ });
128
+
129
+ console.log('\nGET /provisioning/instances/:id/settings:');
101
130
 
102
131
  await ta('owner reads DEFAULTS (public / apply) on a row with empty settings — today\'s behavior', async () => {
103
132
  reset();
@@ -107,6 +107,30 @@ test('a domainless instance gets an honest no-address state', async () => {
107
107
  assert.match(body, /[Nn]o address yet/);
108
108
  });
109
109
 
110
+ test('the manage view is served: identity block + honest not-yet sections (task 1003041)', async () => {
111
+ const { body } = await request('/projects', 'game.test.local');
112
+ assert.match(body, /id="view-manage"/);
113
+ // the four settings homes the sibling manage-wave tasks fill in
114
+ for (const id of ['mVisibility', 'mJoinability', 'mRepoVisibility', 'mDelete']) {
115
+ assert.match(body, new RegExp(`id="${id}"`), `${id} must be served`);
116
+ }
117
+ assert.match(body, /isn’t wired up/, 'each unshipped section says so honestly');
118
+ // the not-found state is one clean answer — never a permission hint
119
+ const missing = (body.match(/<section id="mMissing"[\s\S]*?<\/section>/) || [''])[0];
120
+ assert.ok(missing, 'the not-found state must be served');
121
+ assert.doesNotMatch(missing, /permission|allowed|access|owner/i);
122
+ });
123
+
124
+ test('the repo-visibility section is served live, with the private direction refused (task 1003045)', async () => {
125
+ const { body } = await request('/projects', 'game.test.local');
126
+ assert.match(body, /id="mRepoVis"/, 'the live section body is served');
127
+ assert.match(body, /can’t deploy private repositories/,
128
+ 'the private-repo limitation is stated in plain words');
129
+ assert.doesNotMatch(body, /visibility:\s*'private'/,
130
+ 'no served code path may send the private direction');
131
+ assert.match(body, /repos=1/, 'the reconnect door asks for repository access');
132
+ });
133
+
110
134
  test('the empty state points a new owner at the wizard view — never at a hall', async () => {
111
135
  const { body } = await request('/projects', 'game.test.local');
112
136
  // /create was absorbed into ?view=new (task 1002763); the CTA moves in-page