create-pmos 0.15.0 → 0.16.0
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.
- package/README.md +38 -0
- package/package.json +1 -1
- package/payload/KIT-VERSION +1 -1
- package/workspace/README.md +44 -12
- package/workspace/design-parity.js +358 -0
- package/workspace/lib/find-browser.js +60 -0
- package/workspace/lib/four-panel.js +345 -0
- package/workspace/lib/git-info.js +28 -1
- package/workspace/lib/mobile-nav.js +32 -2
- package/workspace/package-lock.json +29 -2
- package/workspace/package.json +13 -5
- package/workspace/public/index.html +79 -409
- package/workspace/public/pm/app.js +242 -0
- package/workspace/public/pm/data.js +877 -0
- package/workspace/public/pm/dom.js +116 -0
- package/workspace/public/pm/fixture.js +979 -0
- package/workspace/public/pm/mobile.css +200 -0
- package/workspace/public/pm/mobile.js +403 -0
- package/workspace/public/pm/modes.js +1076 -0
- package/workspace/public/pm/panel-detail.js +648 -0
- package/workspace/public/pm/panel-initiatives.js +426 -0
- package/workspace/public/pm/panel-repos.js +460 -0
- package/workspace/public/pm/panel-tabs.js +487 -0
- package/workspace/public/pm/state.js +164 -0
- package/workspace/public/pm/tab-doc.js +279 -0
- package/workspace/public/pm/tab-task.js +83 -0
- package/workspace/public/pm/tab-term.js +560 -0
- package/workspace/public/pm/tokens.css +73 -0
- package/workspace/public/vendor/ibm-plex-mono-500.woff2 +0 -0
- package/workspace/server.js +158 -8
- package/workspace/focus.test.js +0 -650
- package/workspace/handoff.test.js +0 -443
- package/workspace/profiles.test.js +0 -362
- package/workspace/public/app.js +0 -5560
- package/workspace/public/styles.css +0 -2258
- package/workspace/repo-scan.test.js +0 -327
|
@@ -1,362 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/* Claude profile reader — unit suite for workspace/lib/profiles.js (initiative `workspace-profiles`).
|
|
3
|
-
*
|
|
4
|
-
* No framework and no mocks: every case builds a real temp home directory with real files, because
|
|
5
|
-
* the whole point of this module is what the filesystem actually says. A mocked `fs` would let a
|
|
6
|
-
* test pass against a filesystem that does not behave like one — and the two facts this module
|
|
7
|
-
* exists to keep apart (a MISSING directory vs an UNREADABLE one) are distinguished by an errno
|
|
8
|
-
* that only a real filesystem produces.
|
|
9
|
-
*
|
|
10
|
-
* A. absence vs emptiness — the two calm states, held apart
|
|
11
|
-
* B. unreadable — the failure state, held apart from both of the above
|
|
12
|
-
* C. happy path — fields read faithfully, nothing invented
|
|
13
|
-
* D. one bad file (R4) — the good ones survive; the bad one is named
|
|
14
|
-
* E. wrong-typed keys — rejected loudly, never coerced into a false "none declared"
|
|
15
|
-
* F. profileLine (R6/R7) — the five sentences, and the phrase that must never appear
|
|
16
|
-
*/
|
|
17
|
-
'use strict';
|
|
18
|
-
const fs = require('fs');
|
|
19
|
-
const os = require('os');
|
|
20
|
-
const path = require('path');
|
|
21
|
-
const { readProfiles, profileLine, profilesDir } = require('./lib/profiles');
|
|
22
|
-
|
|
23
|
-
/* Assertions are THUNKS, and both the condition and its note are evaluated inside a try.
|
|
24
|
-
*
|
|
25
|
-
* This is not ceremony — it is the lesson `test.js`'s own header records. An assertion that
|
|
26
|
-
* dereferences a value the code was supposed to provide (`r.error.includes(…)`) throws when the
|
|
27
|
-
* code regresses, the throw unwinds past every later section, and the suite exits without a
|
|
28
|
-
* summary. The first time this file was sabotaged on purpose, exactly that happened: the check
|
|
29
|
-
* that mattered went red, the next one threw, and 45 checks never ran. A suite whose failure mode
|
|
30
|
-
* is "stops counting" cannot be trusted to report a pass either, so a throw is recorded as ONE
|
|
31
|
-
* failed check and the run continues.
|
|
32
|
-
*/
|
|
33
|
-
const results = [];
|
|
34
|
-
const ok = (name, cond, extra = '') => {
|
|
35
|
-
let pass = false;
|
|
36
|
-
let note = '';
|
|
37
|
-
try {
|
|
38
|
-
pass = !!(typeof cond === 'function' ? cond() : cond);
|
|
39
|
-
note = String((typeof extra === 'function' ? extra() : extra) ?? '');
|
|
40
|
-
} catch (e) {
|
|
41
|
-
pass = false;
|
|
42
|
-
note = 'threw: ' + String((e && e.message) || e).slice(0, 160);
|
|
43
|
-
}
|
|
44
|
-
results.push({ name, cond: pass });
|
|
45
|
-
console.log(`${pass ? '✓' : '✗'} ${name}${note ? ' — ' + note : ''}`);
|
|
46
|
-
};
|
|
47
|
-
// A truncated run must not be able to print a healthy-looking fraction of a number it silently
|
|
48
|
-
// shrank. Bumped deliberately when checks are added.
|
|
49
|
-
const EXPECTED_CHECKS = 51;
|
|
50
|
-
|
|
51
|
-
// ── fixtures ────────────────────────────────────────────────────────────────────────────────────
|
|
52
|
-
const tmp = [];
|
|
53
|
-
/* home(null) → a home directory with NO .claude at all (the absent case)
|
|
54
|
-
* home({}) → .claude/profiles exists and is empty (the successful-read-of-nothing case)
|
|
55
|
-
* home({...}) → those files written into .claude/profiles; a string body is written verbatim so a
|
|
56
|
-
* test can plant malformed JSON, an object body is stringified.
|
|
57
|
-
*/
|
|
58
|
-
function home(files) {
|
|
59
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pmos-profiles-'));
|
|
60
|
-
tmp.push(root);
|
|
61
|
-
if (files) {
|
|
62
|
-
const dir = path.join(root, '.claude', 'profiles');
|
|
63
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
64
|
-
for (const [name, body] of Object.entries(files)) {
|
|
65
|
-
fs.writeFileSync(path.join(dir, name), typeof body === 'string' ? body : JSON.stringify(body, null, 2));
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return root;
|
|
69
|
-
}
|
|
70
|
-
function cleanup() {
|
|
71
|
-
for (const r of tmp) { try { fs.rmSync(r, { recursive: true, force: true }); } catch {} }
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const WORK = { id: 'claude-work', label: 'claude-work', account: 'pm@company.example', mcp: ['jira', 'confluence', 'slack', 'gworkspace'], env: { A: '1', B: '2', C: '3', D: '4' } };
|
|
75
|
-
const PERSONAL = { id: 'claude-personal', label: 'claude-personal', account: 'me@personal.example', mcp: [], env: { A: '1', B: '2' } };
|
|
76
|
-
|
|
77
|
-
try {
|
|
78
|
-
// ── A. absence and emptiness are DIFFERENT ANSWERS ────────────────────────────────────────────
|
|
79
|
-
{
|
|
80
|
-
const noHome = home(null);
|
|
81
|
-
const a = readProfiles(noHome);
|
|
82
|
-
ok('A1: a missing ~/.claude/profiles reads as a calm stated absence, not a failure',
|
|
83
|
-
a.ok === true && a.absent === true && a.error === null && a.profiles.length === 0,
|
|
84
|
-
JSON.stringify({ ok: a.ok, absent: a.absent, error: a.error }));
|
|
85
|
-
ok('A2: the absence NAMES the path it looked for (R2 — an absence with no path is uncheckable)',
|
|
86
|
-
a.dir === profilesDir(noHome) && a.dir.includes('.claude'), a.dir);
|
|
87
|
-
// The "pure reader" claim, asserted rather than asserted-in-a-comment.
|
|
88
|
-
ok('A3: reading an absent directory creates nothing — the reader is pure',
|
|
89
|
-
!fs.existsSync(path.join(noHome, '.claude')));
|
|
90
|
-
|
|
91
|
-
const emptyHome = home({});
|
|
92
|
-
const b = readProfiles(emptyHome);
|
|
93
|
-
ok('A4: an EMPTY profiles directory is a successful read of nothing, not an absence',
|
|
94
|
-
b.ok === true && b.absent === false && b.error === null && b.profiles.length === 0 && b.broken.length === 0,
|
|
95
|
-
JSON.stringify({ ok: b.ok, absent: b.absent }));
|
|
96
|
-
// The distinction itself. Both are calm and both yield zero chips; they are different facts
|
|
97
|
-
// about the machine and the surface renders different words for them.
|
|
98
|
-
ok('A5: THE DISTINCTION — absent and empty are not the same return value',
|
|
99
|
-
a.absent !== b.absent && a.ok === b.ok && a.profiles.length === b.profiles.length,
|
|
100
|
-
`absent=${a.absent} vs ${b.absent}`);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// ── B. unreadable is a THIRD answer, and it is a failure ──────────────────────────────────────
|
|
104
|
-
{
|
|
105
|
-
// A file sitting where the directory should be → ENOTDIR. This is the portable unreadable
|
|
106
|
-
// case: it does not depend on the uid the suite runs as, which the chmod case below does.
|
|
107
|
-
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'pmos-profiles-'));
|
|
108
|
-
tmp.push(root);
|
|
109
|
-
fs.mkdirSync(path.join(root, '.claude'), { recursive: true });
|
|
110
|
-
fs.writeFileSync(path.join(root, '.claude', 'profiles'), 'not a directory');
|
|
111
|
-
const r = readProfiles(root);
|
|
112
|
-
ok('B1: a FILE where the profiles directory should be reads as a failure, never as an absence',
|
|
113
|
-
r.ok === false && r.absent === false && typeof r.error === 'string' && r.error.length > 0,
|
|
114
|
-
JSON.stringify({ ok: r.ok, absent: r.absent }));
|
|
115
|
-
ok('B2: the failure states its reason and names the path (it must render red with the reason)',
|
|
116
|
-
() => r.error.includes(profilesDir(root)) && /not a directory/.test(r.error), () => r.error);
|
|
117
|
-
// THE FAILURE THIS MODULE EXISTS TO PREVENT. "PMOS could not look" must never be reported as
|
|
118
|
-
// "your machine has no profiles" — that states a capability claim the data does not support.
|
|
119
|
-
ok('B3: THE HONESTY REQUIREMENT — an unreadable directory never reports absent:true',
|
|
120
|
-
r.absent === false && r.ok === false);
|
|
121
|
-
|
|
122
|
-
// Permission failure, where the suite's uid makes it meaningful. Under root, chmod 000 is still
|
|
123
|
-
// readable, so the check is SKIPPED and says so rather than reporting a pass it never earned.
|
|
124
|
-
const canTestPerms = typeof process.getuid === 'function' && process.getuid() !== 0;
|
|
125
|
-
if (canTestPerms) {
|
|
126
|
-
const p = home({});
|
|
127
|
-
fs.chmodSync(profilesDir(p), 0o000);
|
|
128
|
-
const pr = readProfiles(p);
|
|
129
|
-
ok('B4: an unreadable-by-permission directory is a failure with its errno, not an absence',
|
|
130
|
-
pr.ok === false && pr.absent === false && /EACCES|EPERM/.test(pr.error), pr.error);
|
|
131
|
-
fs.chmodSync(profilesDir(p), 0o700);
|
|
132
|
-
} else {
|
|
133
|
-
ok('B4: skipped — running as uid 0, where chmod 000 does not deny a read', true, 'no unprivileged uid');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const nh = readProfiles('');
|
|
137
|
-
ok('B5: no home directory given is a stated failure, not a throw and not a guess',
|
|
138
|
-
nh.ok === false && nh.absent === false && /will not guess/.test(nh.error), nh.error);
|
|
139
|
-
ok('B6: an undefined home directory behaves the same way', readProfiles(undefined).ok === false);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ── C. the happy path: read faithfully, invent nothing ────────────────────────────────────────
|
|
143
|
-
{
|
|
144
|
-
const h = home({ 'claude-work.json': WORK, 'claude-personal.json': PERSONAL });
|
|
145
|
-
const r = readProfiles(h);
|
|
146
|
-
ok('C1: two good descriptors read as two profiles',
|
|
147
|
-
r.ok === true && r.absent === false && r.profiles.length === 2 && r.broken.length === 0,
|
|
148
|
-
r.profiles.map((p) => p.id).join(','));
|
|
149
|
-
ok('C2: chip order is stable (sorted), not filesystem order',
|
|
150
|
-
r.profiles.map((p) => p.id).join(',') === 'claude-personal,claude-work');
|
|
151
|
-
const work = r.profiles.find((p) => p.id === 'claude-work');
|
|
152
|
-
ok('C3: account is carried verbatim and mcp names are carried as declared',
|
|
153
|
-
() => work.account === 'pm@company.example' && work.mcp.join(' ') === 'jira confluence slack gworkspace',
|
|
154
|
-
() => work.account);
|
|
155
|
-
ok('C4: each profile names the descriptor path it came from (R10 footer)',
|
|
156
|
-
() => work.path === path.join(profilesDir(h), 'claude-work.json'), () => work.path);
|
|
157
|
-
ok('C5: the env overlay is read as declared',
|
|
158
|
-
() => Object.keys(work.env).length === 4 && work.env.A === '1');
|
|
159
|
-
|
|
160
|
-
// R1: no default is substituted for a missing key. An absent account must stay absent so the
|
|
161
|
-
// consumer can say "not declared" instead of showing an empty string that looks like a value.
|
|
162
|
-
const h2 = home({ 'bare.json': { id: 'bare' } });
|
|
163
|
-
const b = readProfiles(h2).profiles[0];
|
|
164
|
-
ok('C6: a descriptor with only an id is valid — absent account stays null, nothing invented',
|
|
165
|
-
!!b && b.account === null && Array.isArray(b.mcp) && b.mcp.length === 0 && b.env === null,
|
|
166
|
-
JSON.stringify({ account: b && b.account, mcp: b && b.mcp, env: b && b.env }));
|
|
167
|
-
ok('C7: label falls back to id when absent, and wins when present (R1)',
|
|
168
|
-
() => b.label === 'bare' &&
|
|
169
|
-
readProfiles(home({ 'x.json': { id: 'x', label: 'Work Account' } })).profiles[0].label === 'Work Account');
|
|
170
|
-
|
|
171
|
-
// Files that are not descriptors are not failed descriptors.
|
|
172
|
-
const h3 = home({ 'claude-work.json': WORK, 'README.md': '# notes', 'notes.txt': 'hi' });
|
|
173
|
-
const r3 = readProfiles(h3);
|
|
174
|
-
ok('C8: non-.json files are ignored entirely — not read, not reported as broken',
|
|
175
|
-
r3.profiles.length === 1 && r3.broken.length === 0);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// ── D. R4: one bad file does not take the others down ─────────────────────────────────────────
|
|
179
|
-
{
|
|
180
|
-
const h = home({ 'claude-work.json': WORK, 'claude-personal.json': '{ this is not json' });
|
|
181
|
-
const r = readProfiles(h);
|
|
182
|
-
ok('D1: R4 — a malformed descriptor beside a good one leaves the good one offered',
|
|
183
|
-
r.ok === true && r.profiles.length === 1 && r.profiles[0].id === 'claude-work',
|
|
184
|
-
r.profiles.map((p) => p.id).join(','));
|
|
185
|
-
ok('D2: the bad one is named, with its filename and parse error, so it can render red',
|
|
186
|
-
r.broken.length === 1 && r.broken[0].file === 'claude-personal.json' &&
|
|
187
|
-
/not valid JSON/.test(r.broken[0].error) && r.broken[0].path.endsWith('claude-personal.json'),
|
|
188
|
-
r.broken[0] && r.broken[0].error);
|
|
189
|
-
ok('D3: a directory-level ok stays true — a bad FILE is not a bad DIRECTORY',
|
|
190
|
-
r.ok === true && r.error === null && r.absent === false);
|
|
191
|
-
|
|
192
|
-
// An id that disagrees with its filename is rejected (R1/R4): the id is the identity every
|
|
193
|
-
// other surface cites, so a descriptor whose identity is ambiguous cannot be offered.
|
|
194
|
-
const mm = readProfiles(home({ 'claude-work.json': { id: 'claude-personal', mcp: [] } }));
|
|
195
|
-
ok('D4: an id/filename mismatch is rejected and says which is which',
|
|
196
|
-
mm.profiles.length === 0 && mm.broken.length === 1 && /disagrees with its filename/.test(mm.broken[0].error),
|
|
197
|
-
mm.broken[0] && mm.broken[0].error);
|
|
198
|
-
const noid = readProfiles(home({ 'claude-work.json': { account: 'x' } }));
|
|
199
|
-
ok('D5: a descriptor with no id is rejected — the filename is never substituted as identity',
|
|
200
|
-
noid.profiles.length === 0 && noid.broken.length === 1 && /declares no `id`/.test(noid.broken[0].error));
|
|
201
|
-
|
|
202
|
-
// Shapes that parse as JSON but are not descriptors.
|
|
203
|
-
const shapes = readProfiles(home({ 'a.json': '[]', 'b.json': '"nope"', 'c.json': 'null', 'd.json': '42' }));
|
|
204
|
-
ok('D6: JSON that is not an object (array/string/null/number) is rejected, each named',
|
|
205
|
-
shapes.profiles.length === 0 && shapes.broken.length === 4 &&
|
|
206
|
-
shapes.broken.every((x) => /is not a JSON object/.test(x.error)),
|
|
207
|
-
shapes.broken.map((x) => x.file).join(','));
|
|
208
|
-
|
|
209
|
-
// A DIRECTORY occupying a descriptor's name: reported, not silently skipped, because it holds
|
|
210
|
-
// the name a chip would have and the PM needs to be told why the chip is missing.
|
|
211
|
-
const dh = home({ 'claude-work.json': WORK });
|
|
212
|
-
fs.mkdirSync(path.join(profilesDir(dh), 'claude-personal.json'));
|
|
213
|
-
const dr = readProfiles(dh);
|
|
214
|
-
ok('D7: a directory named like a descriptor is reported as a failed read, not skipped',
|
|
215
|
-
dr.profiles.length === 1 && dr.broken.length === 1 && /could not be read/.test(dr.broken[0].error),
|
|
216
|
-
dr.broken[0] && dr.broken[0].error);
|
|
217
|
-
|
|
218
|
-
// The trap in the return shape: zero good profiles with a non-empty `broken` is a FAILURE that
|
|
219
|
-
// a consumer testing only `profiles.length` would render as the calm empty state of A4.
|
|
220
|
-
const allBad = readProfiles(home({ 'a.json': '{', 'b.json': '{' }));
|
|
221
|
-
ok('D8: every descriptor broken → profiles is empty but absent stays FALSE and broken is populated',
|
|
222
|
-
allBad.ok === true && allBad.absent === false && allBad.profiles.length === 0 && allBad.broken.length === 2,
|
|
223
|
-
JSON.stringify({ absent: allBad.absent, profiles: allBad.profiles.length, broken: allBad.broken.length }));
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ── E. wrong-typed keys are rejected, never coerced ───────────────────────────────────────────
|
|
227
|
-
{
|
|
228
|
-
// The important one. Coercing a malformed `mcp` to [] would make profileLine print "no mcp
|
|
229
|
-
// servers declared in this profile" over a file that plainly declared one — a false statement
|
|
230
|
-
// manufactured from a real declaration, which is worse than the parse error it replaced.
|
|
231
|
-
const m = readProfiles(home({ 'p.json': { id: 'p', mcp: 'jira' } }));
|
|
232
|
-
ok('E1: a wrong-typed `mcp` is rejected — never silently emptied into "none declared"',
|
|
233
|
-
m.profiles.length === 0 && m.broken.length === 1 && /`mcp` is present but is not an array/.test(m.broken[0].error),
|
|
234
|
-
m.broken[0] && m.broken[0].error);
|
|
235
|
-
const m2 = readProfiles(home({ 'p.json': { id: 'p', mcp: ['jira', 7] } }));
|
|
236
|
-
ok('E2: an mcp array holding a non-string is rejected too', m2.profiles.length === 0 && m2.broken.length === 1);
|
|
237
|
-
|
|
238
|
-
const e1 = readProfiles(home({ 'p.json': { id: 'p', env: ['A=1'] } }));
|
|
239
|
-
ok('E3: a wrong-typed `env` is rejected (an array is not an environment overlay)',
|
|
240
|
-
() => e1.profiles.length === 0 && /`env` is present but is not an object/.test(e1.broken[0].error));
|
|
241
|
-
const e2 = readProfiles(home({ 'p.json': { id: 'p', env: { A: 1 } } }));
|
|
242
|
-
ok('E4: a non-string env VALUE is rejected — PMOS will not coerce a value it was not given',
|
|
243
|
-
() => e2.profiles.length === 0 && /`env.A` is not a string/.test(e2.broken[0].error),
|
|
244
|
-
() => e2.broken[0] && e2.broken[0].error);
|
|
245
|
-
const a1 = readProfiles(home({ 'p.json': { id: 'p', account: 42 } }));
|
|
246
|
-
ok('E5: a wrong-typed `account` is rejected rather than stringified into a plausible value',
|
|
247
|
-
() => a1.profiles.length === 0 && /`account` is present but is not a string/.test(a1.broken[0].error));
|
|
248
|
-
const l1 = readProfiles(home({ 'p.json': { id: 'p', label: '' } }));
|
|
249
|
-
ok('E6: an empty `label` is rejected rather than falling back and hiding the bad file',
|
|
250
|
-
() => l1.profiles.length === 0 && /`label` is present/.test(l1.broken[0].error));
|
|
251
|
-
// Keys PMOS does not use are ignored, not rejected — the descriptor format is the agent CLI's,
|
|
252
|
-
// and PMOS is a read-only guest in it (PRD non-goal 3).
|
|
253
|
-
const extra = readProfiles(home({ 'p.json': { id: 'p', someFutureKey: { deep: true } } }));
|
|
254
|
-
ok('E7: keys PMOS does not use are ignored, not treated as corruption',
|
|
255
|
-
extra.profiles.length === 1 && extra.broken.length === 0);
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// ── F. profileLine — R6's sentences and R7's forbidden phrase ─────────────────────────────────
|
|
259
|
-
{
|
|
260
|
-
const h = home({ 'claude-work.json': WORK, 'claude-personal.json': PERSONAL });
|
|
261
|
-
const r = readProfiles(h);
|
|
262
|
-
const work = r.profiles.find((p) => p.id === 'claude-work');
|
|
263
|
-
const personal = r.profiles.find((p) => p.id === 'claude-personal');
|
|
264
|
-
|
|
265
|
-
ok('F1: R6 — a profile with an overlay and declared mcp states what was applied and what was declared',
|
|
266
|
-
profileLine(work) === 'profile: claude-work — applied 4 environment variables · mcp declared by the profile: jira confluence slack gworkspace',
|
|
267
|
-
profileLine(work));
|
|
268
|
-
ok('F2: R7 — a profile declaring no mcp says so as a fact about the FILE, never "mcp: none"',
|
|
269
|
-
profileLine(personal) === 'profile: claude-personal — applied 2 environment variables · no mcp servers declared in this profile',
|
|
270
|
-
profileLine(personal));
|
|
271
|
-
ok('F3: the count is pluralised — one variable is not "1 environment variables"',
|
|
272
|
-
/applied 1 environment variable · /.test(profileLine({ id: 'p', mcp: [], env: { A: '1' } })),
|
|
273
|
-
profileLine({ id: 'p', mcp: [], env: { A: '1' } }));
|
|
274
|
-
|
|
275
|
-
// The case the PRD names as the failure that looks like success: a chip was picked, a terminal
|
|
276
|
-
// opened, and nothing was applied. The line has to say that out loud.
|
|
277
|
-
const noEnv = { id: 'claude-work', label: 'claude-work', mcp: ['jira'], env: null };
|
|
278
|
-
ok('F4: a profile declaring NO env overlay says so — the failure that would otherwise look like success',
|
|
279
|
-
profileLine(noEnv) === "profile: claude-work — the profile declares no environment overlay; this session runs with the workspace's default environment",
|
|
280
|
-
profileLine(noEnv));
|
|
281
|
-
// `env: {}` and a missing `env` key differ on disk and are identical to the PM: nothing was
|
|
282
|
-
// applied. They must converge on the sentence about consequence, not diverge into a count.
|
|
283
|
-
ok('F5: an empty `env: {}` gets the same sentence as a missing env key, never "applied 0 …"',
|
|
284
|
-
profileLine({ id: 'p', mcp: [], env: {} }) === profileLine({ id: 'p', mcp: [], env: null }) &&
|
|
285
|
-
/declares no environment overlay/.test(profileLine({ id: 'p', mcp: [], env: {} })),
|
|
286
|
-
profileLine({ id: 'p', mcp: [], env: {} }));
|
|
287
|
-
ok('F6: "applied 0" never appears in any line', !/applied 0 /.test(profileLine({ id: 'p', mcp: [], env: {} })));
|
|
288
|
-
|
|
289
|
-
// The three no-descriptor cases, kept apart by the argument's own type because a session record
|
|
290
|
-
// is JSON and `undefined` (key absent) survives it differently from `null`.
|
|
291
|
-
ok('F7: an absent recorded profile reads "not recorded" — PMOS did not start this pane',
|
|
292
|
-
profileLine(undefined) === 'profile: not recorded — PMOS did not start this pane', profileLine(undefined));
|
|
293
|
-
ok('F8: an explicit null reads "none was chosen", which is a different fact from "not recorded"',
|
|
294
|
-
profileLine(null) === 'no profile — none was chosen for this session' && profileLine(null) !== profileLine(undefined),
|
|
295
|
-
profileLine(null));
|
|
296
|
-
ok('F9: with a read proving the machine has none configured, the line says so',
|
|
297
|
-
profileLine(null, readProfiles(home(null))) === 'no profile — none was chosen (no profiles are configured on this machine)',
|
|
298
|
-
profileLine(null, readProfiles(home(null))));
|
|
299
|
-
ok('F10: an empty-but-present directory also supports "none are configured"',
|
|
300
|
-
profileLine(null, readProfiles(home({}))) === 'no profile — none was chosen (no profiles are configured on this machine)');
|
|
301
|
-
// …but a read that FAILED, or one that found broken files, does not support that claim: the
|
|
302
|
-
// profiles may well be configured and unreadable. Claiming none exist would be inventing.
|
|
303
|
-
const failedRead = readProfiles('');
|
|
304
|
-
ok('F11: a FAILED read never licenses "no profiles are configured on this machine"',
|
|
305
|
-
profileLine(null, failedRead) === 'no profile — none was chosen for this session',
|
|
306
|
-
profileLine(null, failedRead));
|
|
307
|
-
ok('F12: a read that found only BROKEN descriptors does not claim none are configured',
|
|
308
|
-
profileLine(null, readProfiles(home({ 'a.json': '{' }))) === 'no profile — none was chosen for this session');
|
|
309
|
-
|
|
310
|
-
// A bare id string cannot support "applied N variables" — what was applied is a fact about the
|
|
311
|
-
// descriptor. R9's respawn case depends on this refusal.
|
|
312
|
-
ok('F13: a bare id string is refused, not rendered — what was applied is a fact about the descriptor',
|
|
313
|
-
profileLine('claude-work') === 'profile: unreadable — the recorded value is not a profile descriptor',
|
|
314
|
-
profileLine('claude-work'));
|
|
315
|
-
ok('F14: garbage input yields a stated-unreadable line rather than a throw or a false claim',
|
|
316
|
-
profileLine(42) === profileLine('x') && profileLine([]) === profileLine('x') &&
|
|
317
|
-
profileLine({ mcp: [] }) === 'profile: unreadable — the recorded descriptor names no profile',
|
|
318
|
-
profileLine({ mcp: [] }));
|
|
319
|
-
|
|
320
|
-
// R7 as a standing regression over EVERY line this module can emit.
|
|
321
|
-
const everyLine = [
|
|
322
|
-
profileLine(work), profileLine(personal), profileLine(noEnv), profileLine(undefined),
|
|
323
|
-
profileLine(null), profileLine(null, r), profileLine('x'), profileLine({ mcp: [] }),
|
|
324
|
-
profileLine({ id: 'p', mcp: [], env: { A: '1' } }),
|
|
325
|
-
];
|
|
326
|
-
ok('F15a: R7 — the phrase "mcp: none" appears in none of the lines these descriptors produce',
|
|
327
|
-
everyLine.every((l) => !/mcp:\s*none/i.test(l)), `${everyLine.length} lines checked`);
|
|
328
|
-
// …and the claim F15a used to make — "no line this module CAN produce" — is not something nine
|
|
329
|
-
// sampled descriptors can establish. It is a claim about the module, so assert it about the
|
|
330
|
-
// module: the phrase must not exist as a literal anywhere the formatter could emit it. That is
|
|
331
|
-
// the whole space, because the only other route into a line is PM-authored text echoed from a
|
|
332
|
-
// descriptor's own `label`/`mcp` — a profile literally named "mcp: none" quotes the PM back to
|
|
333
|
-
// themself, which is not the module asserting a fact about the world and is not what R7 bans.
|
|
334
|
-
// Sampling had been dressed as exhaustiveness; the fix is to test the right object, not to
|
|
335
|
-
// widen the sample.
|
|
336
|
-
const src = fs.readFileSync(path.join(__dirname, 'lib', 'profiles.js'), 'utf8');
|
|
337
|
-
const emittable = src.split('\n')
|
|
338
|
-
.filter((l) => !/^\s*(\*|\/\/|\/\*)/.test(l)) // prose ABOUT the ban is not the ban
|
|
339
|
-
.join('\n');
|
|
340
|
-
ok('F15b: R7 REGRESSION — "mcp: none" is not a literal anywhere the formatter could emit it',
|
|
341
|
-
!/mcp:\s*none/i.test(emittable), 'lib/profiles.js scanned, comments excluded');
|
|
342
|
-
ok('F16: no line claims a connection, a status or a sync time (no MCP client exists)',
|
|
343
|
-
everyLine.every((l) => !/connected|synced|last sync|reachable|online/i.test(l)));
|
|
344
|
-
ok('F17: every line is a non-empty single line — nothing renders blank',
|
|
345
|
-
everyLine.every((l) => typeof l === 'string' && l.trim().length > 0 && !l.includes('\n')));
|
|
346
|
-
}
|
|
347
|
-
} finally {
|
|
348
|
-
cleanup();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
const failed = results.filter((r) => !r.cond);
|
|
352
|
-
console.log(`\n${results.length - failed.length}/${results.length} checks passed`);
|
|
353
|
-
if (failed.length) console.log('failed:\n ' + failed.map((f) => f.name).join('\n '));
|
|
354
|
-
|
|
355
|
-
// A run that ended early — a throw outside a thunk, an early return — would otherwise print a
|
|
356
|
-
// green fraction of a shrunken denominator. Report the shortfall as a failure in its own right.
|
|
357
|
-
if (results.length < EXPECTED_CHECKS) {
|
|
358
|
-
console.log(`\n✗ SUITE TRUNCATED: ${results.length} checks ran, ${EXPECTED_CHECKS} expected — ` +
|
|
359
|
-
'the run ended early, so this result is not a pass.');
|
|
360
|
-
process.exit(1);
|
|
361
|
-
}
|
|
362
|
-
process.exit(failed.length ? 1 : 0);
|