@bongos/core 1.20.9 → 1.20.11
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/.bongos-core.json +101 -96
- package/.claude/skills/ask-for-help/SKILL.md +6 -0
- package/.claude/skills/backlog-review/SKILL.md +6 -0
- package/.claude/skills/blocker-review/SKILL.md +6 -0
- package/.claude/skills/blocker-solve/SKILL.md +6 -0
- package/.claude/skills/bug-triage/SKILL.md +6 -0
- package/.claude/skills/builder-backup/SKILL.md +6 -0
- package/.claude/skills/builder-claim/SKILL.md +6 -0
- package/.claude/skills/builder-cost/SKILL.md +6 -0
- package/.claude/skills/builder-end/SKILL.md +6 -0
- package/.claude/skills/builder-exit/SKILL.md +6 -0
- package/.claude/skills/builder-key/SKILL.md +6 -0
- package/.claude/skills/builder-reauth/SKILL.md +6 -0
- package/.claude/skills/builder-redteam/SKILL.md +6 -0
- package/.claude/skills/builder-release/SKILL.md +6 -0
- package/.claude/skills/builder-sequence/SKILL.md +6 -0
- package/.claude/skills/builder-setup/SKILL.md +6 -0
- package/.claude/skills/builder-ship/SKILL.md +6 -0
- package/.claude/skills/builder-stage/SKILL.md +6 -0
- package/.claude/skills/builder-start/SKILL.md +6 -0
- package/.claude/skills/collab-review/SKILL.md +6 -0
- package/.claude/skills/design/SKILL.md +6 -0
- package/.claude/skills/design-sync/SKILL.md +6 -0
- package/.claude/skills/feedback/SKILL.md +6 -0
- package/.claude/skills/figma-design-sync/SKILL.md +6 -0
- package/.claude/skills/fix-task/SKILL.md +6 -0
- package/.claude/skills/goal-create/SKILL.md +6 -0
- package/.claude/skills/goal-review/SKILL.md +6 -0
- package/.claude/skills/goal-uat/SKILL.md +6 -0
- package/.claude/skills/grade-audit/SKILL.md +6 -0
- package/.claude/skills/grade-recover/SKILL.md +6 -0
- package/.claude/skills/grader-health/SKILL.md +6 -0
- package/.claude/skills/idea-triage/SKILL.md +6 -0
- package/.claude/skills/merge-mode/SKILL.md +6 -0
- package/.claude/skills/new-project/SKILL.md +6 -0
- package/.claude/skills/otb-character-review/SKILL.md +6 -0
- package/.claude/skills/otb-design-review/SKILL.md +6 -0
- package/.claude/skills/otb-feedback-capture/SKILL.md +6 -0
- package/.claude/skills/otb-figma-sync/SKILL.md +6 -0
- package/.claude/skills/otb-tile-generate/SKILL.md +6 -0
- package/.claude/skills/planning-session/SKILL.md +6 -0
- package/.claude/skills/priority-session/SKILL.md +6 -0
- package/.claude/skills/read-session-export/SKILL.md +6 -0
- package/.claude/skills/recall/SKILL.md +6 -0
- package/.claude/skills/scan-before-install/SKILL.md +6 -0
- package/.claude/skills/session-handoff/SKILL.md +6 -0
- package/.claude/skills/ship-check/SKILL.md +6 -0
- package/.claude/skills/status/SKILL.md +6 -0
- package/.claude/skills/strand-fix/SKILL.md +6 -0
- package/.claude/skills/tweak/SKILL.md +6 -0
- package/.claude/skills/worktree-clean/SKILL.md +6 -0
- package/clients/bongos-client/README.md +1 -1
- package/clients/bongos-client/bongos-client.global.js +2 -0
- package/clients/bongos-client/index.cjs +2 -0
- package/clients/bongos-client/index.d.ts +3 -0
- package/clients/bongos-client/index.mjs +2 -0
- package/docs/api/openapi.json +50 -3
- package/docs/api-reference.md +3 -2
- package/docs/copy-inventory.md +60 -52
- package/docs/copy-registry.json +126 -54
- package/docs/module-api-changelog.md +4 -0
- package/docs/page-inventory.json +2 -1
- package/docs/page-readings.json +131 -119
- package/modules/hall-ui/public/panel.css +4 -4
- package/modules/hall-ui/public/settings-specialities.js +139 -2
- package/modules/hall-ui/public/settings.css +26 -0
- package/modules/hall-ui/public/settings.html +4 -0
- package/modules/hall-ui/public/settings.states.json +1 -0
- package/modules/hall-ui/records/panel-family.md +2 -0
- package/modules/specialities/routes/specialities.js +16 -0
- package/modules/ui-design/skills/brandkit/SKILL.md +6 -0
- package/modules/ui-design/skills/design-taste-frontend/SKILL.md +6 -0
- package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +6 -0
- package/modules/ui-design/skills/gpt-taste/SKILL.md +6 -0
- package/modules/ui-design/skills/high-end-visual-design/SKILL.md +6 -0
- package/modules/ui-design/skills/image-to-code/SKILL.md +6 -0
- package/modules/ui-design/skills/imagegen-frontend-mobile/SKILL.md +6 -0
- package/modules/ui-design/skills/imagegen-frontend-web/SKILL.md +6 -0
- package/modules/ui-design/skills/impeccable/SKILL.md +6 -0
- package/modules/ui-design/skills/industrial-brutalist-ui/SKILL.md +6 -0
- package/modules/ui-design/skills/minimalist-ui/SKILL.md +6 -0
- package/modules/ui-design/skills/redesign-existing-projects/SKILL.md +6 -0
- package/modules/ui-design/skills/stitch-design-taste/SKILL.md +6 -0
- package/modules/ui-design/skills/style/SKILL.md +6 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/hall-preview/server.js +4 -0
- package/src/module-api.js +1 -1
- package/tests/speciality_offer_ui.mjs +185 -0
- package/tests/speciality_skills.mjs +51 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// tests/speciality_offer_ui.mjs — a speciality's OWNER chooses what it offers,
|
|
2
|
+
// from the hall (task 1004404; ADR 0310 §1).
|
|
3
|
+
//
|
|
4
|
+
// Before this only the API could set a speciality's offered skills, so nothing
|
|
5
|
+
// made in the hall offered any and the adoption walkthrough never appeared. These
|
|
6
|
+
// pin that the owner can choose on create AND afterwards, that each choice shows
|
|
7
|
+
// the skill's plain description, that only installed skills are choices, and that
|
|
8
|
+
// a skill removed since is named rather than silently kept.
|
|
9
|
+
//
|
|
10
|
+
// Like tests/speciality_walkthrough_ui.mjs this EXECUTES the real
|
|
11
|
+
// modules/hall-ui/public/settings-specialities.js in a vm under a minimal DOM
|
|
12
|
+
// stub against a fake API, and drives the delegated handlers the page wires.
|
|
13
|
+
//
|
|
14
|
+
// Run: node tests/speciality_offer_ui.mjs
|
|
15
|
+
import assert from 'node:assert/strict';
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import vm from 'node:vm';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { makeRunner } from './helpers.mjs';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const SRC = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings-specialities.js'), 'utf8');
|
|
24
|
+
const HTML = fs.readFileSync(path.join(ROOT, 'modules/hall-ui/public/settings.html'), 'utf8');
|
|
25
|
+
const { test, summary } = makeRunner();
|
|
26
|
+
|
|
27
|
+
const INSTALLED = [
|
|
28
|
+
{ name: 'builder-cost', does: 'Records money spent on the project.', cost: 'Free. It only adds an entry.' },
|
|
29
|
+
{ name: 'recall', does: 'Searches everything the project already knows.', cost: 'Free, and it changes nothing.' },
|
|
30
|
+
{ name: 'status', does: 'Shows how far along a version is.', cost: 'Free, and it changes nothing.' },
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const mine = (over = {}) => ({ id: 12, name: 'Pricing', discipline: 'engineer', summary: '', contract_md: '', visibility: 'private', adoptable: true, mine: true, skills: ['recall', 'retired-skill'], ...over });
|
|
34
|
+
const theirs = (over = {}) => ({ id: 9, name: 'Economics', discipline: 'ideator', summary: '', contract_md: '', visibility: 'public', adoptable: true, mine: false, skills: ['recall'], ...over });
|
|
35
|
+
|
|
36
|
+
// The checked boxes a form would submit, read off the rendered markup.
|
|
37
|
+
const checkedIn = (html) => [...html.matchAll(/<input type="checkbox" name="skill" value="([^"]+)" checked/g)].map((m) => m[1]);
|
|
38
|
+
const fakeForm = (dataset, values) => ({
|
|
39
|
+
dataset,
|
|
40
|
+
querySelectorAll: (sel) => (sel === 'input[name="skill"]:checked' ? values.map((value) => ({ value })) : []),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
async function boot({ catalog = [mine(), theirs()] } = {}) {
|
|
44
|
+
const requests = [];
|
|
45
|
+
const toasts = [];
|
|
46
|
+
const els = new Map();
|
|
47
|
+
const handlers = new Map();
|
|
48
|
+
const makeEl = (id) => ({
|
|
49
|
+
id, hidden: id === 'specialities-scroll', innerHTML: '', textContent: '', elements: {}, dataset: {}, open: false,
|
|
50
|
+
addEventListener(type, fn) { handlers.set(`${id}:${type}`, fn); },
|
|
51
|
+
});
|
|
52
|
+
for (const id of ['specialities-scroll', 'specialities-rows', 'speciality-suggestions', 'specialities-status', 'speciality-create', 'speciality-create-offer', 'speciality-create-skills']) els.set(id, makeEl(id));
|
|
53
|
+
// The create form's fields, as settings.html declares them.
|
|
54
|
+
const create = els.get('speciality-create');
|
|
55
|
+
const field = (value) => ({ value, addEventListener() {} });
|
|
56
|
+
create.elements = { name: field(''), discipline: field(''), summary: field(''), contract_md: field(''), visibility: field('private'), sellable: { checked: false, disabled: false, addEventListener() {} } };
|
|
57
|
+
create.reset = () => { create.elements.name.value = ''; };
|
|
58
|
+
create.querySelectorAll = () => [];
|
|
59
|
+
|
|
60
|
+
const fetch = async (url, opts = {}) => {
|
|
61
|
+
const method = opts.method || 'GET';
|
|
62
|
+
const p = url.replace('/api/bongos', '');
|
|
63
|
+
requests.push({ method, path: p, body: opts.body ? JSON.parse(opts.body) : undefined });
|
|
64
|
+
let body = {};
|
|
65
|
+
if (method === 'GET' && p === '/specialities') body = { specialities: catalog };
|
|
66
|
+
else if (method === 'GET' && p === '/me/specialities') body = { adoptions: [] };
|
|
67
|
+
else if (method === 'GET' && p === '/specialities/offered') body = { suggestions: [] };
|
|
68
|
+
else if (method === 'GET' && p === '/specialities/installed-skills') body = { skills: INSTALLED };
|
|
69
|
+
else if (method === 'PATCH') body = { speciality: { id: 12 } };
|
|
70
|
+
else if (method === 'POST' && p === '/specialities') body = { speciality: { id: 13 } };
|
|
71
|
+
return { ok: true, status: 200, json: async () => body };
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const document = { readyState: 'complete', getElementById: (id) => els.get(id) || null, addEventListener() {}, querySelector: () => null };
|
|
75
|
+
const window = {
|
|
76
|
+
OTB: {
|
|
77
|
+
escapeHtml: (s) => String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"'),
|
|
78
|
+
$: () => null, toast: (m) => toasts.push(m), roleAxisLabel: () => 'Role',
|
|
79
|
+
},
|
|
80
|
+
location: { assign() {} },
|
|
81
|
+
};
|
|
82
|
+
vm.runInNewContext(SRC, { window, document, fetch, JSON, Promise, Map, Set, String, Number, Array, Object, Math, RegExp, console });
|
|
83
|
+
const settle = async () => { for (let i = 0; i < 8; i++) await new Promise((r) => setTimeout(r, 0)); };
|
|
84
|
+
await settle();
|
|
85
|
+
|
|
86
|
+
const rows = els.get('specialities-rows');
|
|
87
|
+
async function click(act, id) {
|
|
88
|
+
const btn = { dataset: { act, id: String(id) } };
|
|
89
|
+
handlers.get('specialities-rows:click')({ target: { closest: (sel) => (sel === 'button[data-act]' ? btn : null) } });
|
|
90
|
+
await settle();
|
|
91
|
+
}
|
|
92
|
+
async function submitOffer(id, values) {
|
|
93
|
+
const form = fakeForm({ id: String(id) }, values);
|
|
94
|
+
handlers.get('specialities-rows:submit')({ target: { closest: (sel) => (sel === 'form.speciality-offer' ? form : null) }, preventDefault() {} });
|
|
95
|
+
await settle();
|
|
96
|
+
}
|
|
97
|
+
return { els, rows, requests, toasts, click, submitOffer, handlers, create, settle };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const writes = (h) => h.requests.filter((r) => r.method !== 'GET');
|
|
101
|
+
const installedReads = (h) => h.requests.filter((r) => r.path === '/specialities/installed-skills').length;
|
|
102
|
+
|
|
103
|
+
await test('only a speciality you own has an "Offered skills" control', async () => {
|
|
104
|
+
const h = await boot();
|
|
105
|
+
const buttons = [...h.rows.innerHTML.matchAll(/data-act="offer" data-id="(\d+)"/g)].map((m) => m[1]);
|
|
106
|
+
assert.deepEqual(buttons, ['12']);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
await test('opening it lists every installed skill with its plain description and cost, the current offer ticked', async () => {
|
|
110
|
+
const h = await boot();
|
|
111
|
+
assert.equal(installedReads(h), 0, 'nothing is fetched until the owner asks');
|
|
112
|
+
await h.click('offer', 12);
|
|
113
|
+
assert.equal(installedReads(h), 1);
|
|
114
|
+
const html = h.rows.innerHTML;
|
|
115
|
+
for (const k of INSTALLED) {
|
|
116
|
+
assert.ok(html.includes(`value="${k.name}"`), `${k.name} is a choice`);
|
|
117
|
+
assert.ok(html.includes(k.does), `${k.name} shows what it does`);
|
|
118
|
+
assert.ok(html.includes(k.cost), `${k.name} shows what it costs`);
|
|
119
|
+
}
|
|
120
|
+
assert.deepEqual(checkedIn(html), ['recall'], 'what it offers today is ticked');
|
|
121
|
+
assert.match(html, /Offering a skill turns nothing on/);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
await test('a skill it offers that the instance no longer has is named, not offered as a choice', async () => {
|
|
125
|
+
const h = await boot();
|
|
126
|
+
await h.click('offer', 12);
|
|
127
|
+
assert.ok(!h.rows.innerHTML.includes('value="retired-skill"'), 'not a checkbox');
|
|
128
|
+
assert.match(h.rows.innerHTML, /No longer on this project, so saving drops it from the offer: <code>retired-skill<\/code>/);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await test('saving sends exactly the ticked skills as one PATCH, and nothing is written before', async () => {
|
|
132
|
+
const h = await boot();
|
|
133
|
+
await h.click('offer', 12);
|
|
134
|
+
assert.equal(writes(h).length, 0);
|
|
135
|
+
await h.submitOffer(12, ['recall', 'status']);
|
|
136
|
+
const w = writes(h);
|
|
137
|
+
assert.equal(w.length, 1);
|
|
138
|
+
assert.equal(w[0].method, 'PATCH');
|
|
139
|
+
assert.equal(w[0].path, '/specialities/12');
|
|
140
|
+
assert.deepEqual(w[0].body, { skills: ['recall', 'status'] });
|
|
141
|
+
assert.ok(h.toasts.some((m) => /offers 2 skills/.test(m)));
|
|
142
|
+
assert.ok(!/class="speciality-offer"/.test(h.rows.innerHTML), 'the editor closes on save');
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
await test('offering nothing is a choice too', async () => {
|
|
146
|
+
const h = await boot();
|
|
147
|
+
await h.click('offer', 12);
|
|
148
|
+
await h.submitOffer(12, []);
|
|
149
|
+
assert.deepEqual(writes(h)[0].body, { skills: [] });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
await test('the installed list is read once per page, however often the editor opens', async () => {
|
|
153
|
+
const h = await boot();
|
|
154
|
+
await h.click('offer', 12);
|
|
155
|
+
await h.click('offer', 12); // close
|
|
156
|
+
await h.click('offer', 12); // open again
|
|
157
|
+
assert.equal(installedReads(h), 1);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
await test('the create form offers the same choices, filled on first open, and sends them with the new speciality', async () => {
|
|
161
|
+
const h = await boot();
|
|
162
|
+
const box = h.els.get('speciality-create-offer');
|
|
163
|
+
const host = h.els.get('speciality-create-skills');
|
|
164
|
+
box.open = true;
|
|
165
|
+
h.handlers.get('speciality-create-offer:toggle')();
|
|
166
|
+
await h.settle();
|
|
167
|
+
for (const k of INSTALLED) assert.ok(host.innerHTML.includes(`value="${k.name}"`) && host.innerHTML.includes(k.does));
|
|
168
|
+
assert.deepEqual(checkedIn(host.innerHTML), [], 'a new speciality starts offering nothing');
|
|
169
|
+
h.create.elements.name.value = 'New one';
|
|
170
|
+
h.create.elements.discipline.value = 'engineer';
|
|
171
|
+
h.create.querySelectorAll = (sel) => (sel === 'input[name="skill"]:checked' ? [{ value: 'status' }] : []);
|
|
172
|
+
h.handlers.get('speciality-create:submit')({ preventDefault() {} });
|
|
173
|
+
await h.settle();
|
|
174
|
+
const post = writes(h).find((r) => r.method === 'POST' && r.path === '/specialities');
|
|
175
|
+
assert.ok(post, 'created');
|
|
176
|
+
assert.deepEqual(post.body.skills, ['status']);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
await test('settings.html carries the create form\'s picker inside the form it submits with', () => {
|
|
180
|
+
const form = HTML.slice(HTML.indexOf('<form id="speciality-create"'), HTML.indexOf('</form>', HTML.indexOf('<form id="speciality-create"')));
|
|
181
|
+
assert.match(form, /<details id="speciality-create-offer"/);
|
|
182
|
+
assert.match(form, /id="speciality-create-skills"/);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
summary();
|
|
@@ -73,6 +73,48 @@ await test('every skill in the real tree reads a real description — none is a
|
|
|
73
73
|
assert.deepEqual(bad, [], `these would teach nothing in the walkthrough: ${bad.join(', ')}`);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
// task 1004403: every skill a speciality could offer — every core skill and
|
|
77
|
+
// every module's, enabled here or not — carries its own people-facing lines, so
|
|
78
|
+
// the walkthrough never has to derive them and a new skill cannot arrive without.
|
|
79
|
+
const { JARGON_TERMS } = require('../modules/sessions/accessibility.js');
|
|
80
|
+
// The words task 1004403 named as the ones the derived text leaked, plus the
|
|
81
|
+
// repo nouns a newcomer would not know.
|
|
82
|
+
const SKILL_JARGON = ['DB', 'grep', 'grepping', 'rank-scoped', 'status=', 'residue', 'criterion', 'criteria', 'touches[]', 'branch', 'commit', 'diff', 'SHA', 'CI', 'repo', 'PR'];
|
|
83
|
+
const everySkill = () => skills.installedSkills({ coreRoot: ROOT, isModuleEnabled: () => true });
|
|
84
|
+
|
|
85
|
+
await test('every installable skill carries plain, reach-for and cost — none is derived', () => {
|
|
86
|
+
const all = [...everySkill().values()];
|
|
87
|
+
assert.ok(all.length >= 60, `the whole tree was read (${all.length})`);
|
|
88
|
+
const missing = all.flatMap((s) => ['plain', 'reachFor', 'cost'].filter((k) => !s[k] || s[k].length < 15).map((k) => `${s.name}.${k}`));
|
|
89
|
+
assert.deepEqual(missing, [], 'add plain: / reach-for: / cost: to the SKILL.md frontmatter, written for someone with no engineering background');
|
|
90
|
+
const derived = all.filter((s) => { const d = skills.explainSkill(s).derived; return d.does || d.reachFor || d.cost; }).map((s) => s.name);
|
|
91
|
+
assert.deepEqual(derived, [], 'the walkthrough shows the author\'s own words for every skill');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await test('the people-facing lines stay out of the vocabulary they exist to translate', () => {
|
|
95
|
+
const banned = [...JARGON_TERMS, ...SKILL_JARGON];
|
|
96
|
+
const hits = [];
|
|
97
|
+
for (const s of everySkill().values()) {
|
|
98
|
+
for (const k of ['plain', 'reachFor', 'cost']) {
|
|
99
|
+
for (const term of banned) {
|
|
100
|
+
const re = new RegExp(`(^|[^A-Za-z])${term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}([^A-Za-z]|$)`, /^[A-Z+-]+$/.test(term) ? '' : 'i');
|
|
101
|
+
if (re.test(s[k] || '')) hits.push(`${s.name}.${k}: "${term}"`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
assert.deepEqual(hits, []);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await test('a skill with a paid step says so in its cost line', () => {
|
|
109
|
+
// The ones whose run spends money: every hand-in pays for a quality review,
|
|
110
|
+
// and image generation is billed per image. Named, so dropping the word from
|
|
111
|
+
// one of them is caught rather than trusted.
|
|
112
|
+
const paid = ['builder-ship', 'fix-task', 'builder-sequence', 'grade-recover', 'tweak', 'otb-tile-generate', 'imagegen-frontend-web', 'imagegen-frontend-mobile'];
|
|
113
|
+
const all = everySkill();
|
|
114
|
+
const quiet = paid.filter((n) => !/\bpaid\b|costs money/i.test((all.get(n) || {}).cost || ''));
|
|
115
|
+
assert.deepEqual(quiet, []);
|
|
116
|
+
});
|
|
117
|
+
|
|
76
118
|
await test('explainSkill splits a model-facing description into what it does, when, and what it costs', () => {
|
|
77
119
|
const e = skills.explainSkill({
|
|
78
120
|
name: 'goal-review', source: 'core',
|
|
@@ -262,6 +304,15 @@ try {
|
|
|
262
304
|
store.speciality = saved;
|
|
263
305
|
});
|
|
264
306
|
|
|
307
|
+
await test('GET /specialities/installed-skills lists every installed skill, explained, for the owner\'s picker (task 1004404)', async () => {
|
|
308
|
+
const r = await call('GET', '/specialities/installed-skills');
|
|
309
|
+
assert.equal(r.status, 200, `not captured by /specialities/:id: ${JSON.stringify(r.body)}`);
|
|
310
|
+
assert.deepEqual(r.body.skills.map((s) => s.name), ['costing', 'recall', 'status'], 'every installed skill, by name — the choices are what the instance has');
|
|
311
|
+
const recall = r.body.skills.find((s) => s.name === 'recall');
|
|
312
|
+
assert.equal(recall.does, 'Search what the project knows. Read-only.', 'the same explanation the walkthrough shows');
|
|
313
|
+
assert.match(recall.cost, /^Read-only/);
|
|
314
|
+
});
|
|
315
|
+
|
|
265
316
|
await test('PUT /specialities/:id/skills needs an active adoption', async () => {
|
|
266
317
|
store.adoption = null;
|
|
267
318
|
const r = await call('PUT', '/specialities/9/skills', { enabled_skills: ['recall'] });
|