dsh-molbio-tools 0.22.0 → 0.22.2
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/CHANGELOG.md +121 -0
- package/README.md +21 -9
- package/build/client-entry.mjs +159 -0
- package/build/panel-core.mjs +38 -0
- package/docs/client-panel.md +21 -5
- package/docs/history.md +21 -0
- package/docs/maintainer.md +1 -1
- package/docs/roadmap.md +12 -2
- package/docs/store-evidence.md +29 -18
- package/docs/workflow.md +1 -1
- package/index.mjs +39 -8
- package/lib/client.js +198 -1
- package/package.json +5 -3
- package/test/client.mjs +62 -3
- package/test/contract.mjs +139 -17
- package/test/map-card.mjs +4 -2
- package/test/panel-render.mjs +77 -1
- package/test/smoke.mjs +49 -0
package/docs/store-evidence.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
```jsonc
|
|
12
12
|
"engines": {
|
|
13
|
-
"node": ">=
|
|
13
|
+
"node": ">=24",
|
|
14
14
|
"dsh": ">=0.2.1-rc.2 <0.3.0-0"
|
|
15
15
|
},
|
|
16
16
|
"dsh": {
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
"dshReleases": {
|
|
19
19
|
"0.2.1-rc.1": "unknown",
|
|
20
20
|
"0.2.1-rc.2": "compatible",
|
|
21
|
-
"0.2.1-rc.3": "compatible"
|
|
21
|
+
"0.2.1-rc.3": "compatible",
|
|
22
|
+
"0.2.1-rc.4": "unknown",
|
|
23
|
+
"0.2.1-rc.5": "compatible"
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
}
|
|
@@ -29,21 +31,23 @@
|
|
|
29
31
|
`@deepseek-ai/dsh-app-boot` 的 `unmetRequirements`):带 prerelease 选项时 `<0.3.0`
|
|
30
32
|
会放行 `0.3.0-alpha.1`,而 `-0` 不会。
|
|
31
33
|
|
|
32
|
-
### 为什么 rc.2 / rc.3 是 compatible、rc.1 是 unknown
|
|
34
|
+
### 为什么 rc.2 / rc.3 / rc.5 是 compatible、rc.1 / rc.4 是 unknown
|
|
33
35
|
|
|
34
36
|
| 版本 | 记录 | 依据 |
|
|
35
37
|
| --- | --- | --- |
|
|
36
38
|
| 0.2.1-rc.1 | unknown | 早于本包基线(0.20.1 钉 `0.2.0-rc.2`),未在此版本上运行过;同时也不在 `engines.dsh` 范围内 |
|
|
37
39
|
| 0.2.1-rc.2 | compatible | 0.21.0 / 0.21.1 的发布基线;本文件第 3 节的一次性 Profile 流程在 rc.2 上跑过;`contract` / `preset-health` / `smoke` 均通过 |
|
|
38
|
-
| 0.2.1-rc.3 | compatible | 0.22.0
|
|
40
|
+
| 0.2.1-rc.3 | compatible | 0.22.0 的发布基线;同一套一次性 Profile 流程在 rc.3 上重跑通过(安装 → 启动 HTTP 200 → 卸载无残留);`contract` / `preset-health` / `drift-probe` / `benchmark-profile` / `smoke` 在 rc.3 上全绿。上游 `standard` 行清单与 rc.2 **逐字节一致** |
|
|
41
|
+
| 0.2.1-rc.4 | unknown | 没有在真实安装上跑过;不能因为"补丁号在 `engines.dsh` 范围内"就写成 compatible |
|
|
42
|
+
| 0.2.1-rc.5 | compatible | 0.22.2 的发布基线(CI 钉该版本)。在本机 DSH 桌面版 0.2.1-rc.5 上重跑:安装 → 启动 HTTP 200 → `agentPresets/list` 的 `broken` 为空且 molbio-lab 的 `tool-molbio` 为 `fiberPhase: "active"` → 卸载无残留;`contract` 17 / `preset-health` 30 行 / `drift-probe` 16 / `smoke` 60 工具 / `bench:offline` 61 题全绿。上游 `standard` 与 rc.3 **逐字节一致**(6799 B,同一 sha256) |
|
|
39
43
|
|
|
40
44
|
## 2. 环境
|
|
41
45
|
|
|
42
46
|
| 项 | 值 |
|
|
43
47
|
| --- | --- |
|
|
44
|
-
| DSH | `0.2.1-rc.
|
|
45
|
-
| Node.js | `
|
|
46
|
-
| pnpm | `11.
|
|
48
|
+
| DSH | `0.2.1-rc.5`(桌面版 `app.asar`,同一份代码也是 CI 的 npm 基线) |
|
|
49
|
+
| Node.js | `v24.21.0`(DSH 桌面版内嵌的 Node,Electron 44.7.0) |
|
|
50
|
+
| pnpm | `11.28.5`(DSH bundle 内) |
|
|
47
51
|
| 平台 | Windows(win32 x64) |
|
|
48
52
|
|
|
49
53
|
## 3. 一次性 Profile 的完整流程
|
|
@@ -52,9 +56,9 @@
|
|
|
52
56
|
|
|
53
57
|
```bash
|
|
54
58
|
export DSH_HOME=<workspace>/.tmp/dsh-store-check
|
|
55
|
-
npm pack --pack-destination "$DSH_HOME" # 得到 dsh-molbio-tools-0.22.
|
|
59
|
+
npm pack --pack-destination "$DSH_HOME" # 得到 dsh-molbio-tools-0.22.2.tgz
|
|
56
60
|
dsh --profile molbio-tgz-check --from-default-profile web --dump-config
|
|
57
|
-
dsh plugin --profile molbio-tgz-check add "$DSH_HOME/dsh-molbio-tools-0.22.
|
|
61
|
+
dsh plugin --profile molbio-tgz-check add "$DSH_HOME/dsh-molbio-tools-0.22.2.tgz"
|
|
58
62
|
dsh --profile molbio-tgz-check --dump-config # 安装后的组合树
|
|
59
63
|
dsh --profile molbio-tgz-check --no-open --port 0 # 启动,打印 URL
|
|
60
64
|
curl "http://127.0.0.1:<port>/?token=<token>" # 读回页面
|
|
@@ -65,18 +69,18 @@ dsh --profile molbio-tgz-check --dump-config # 卸载后的组合树
|
|
|
65
69
|
安装用 **tarball** 而不是目录,因为 tarball 就是发布物本身:它能同时证明 `files`
|
|
66
70
|
白名单带全了运行文件(`host.mjs`、`lib/client.js`、单个 `cordis.patch.yml` 的两个 insert、preset)。
|
|
67
71
|
|
|
68
|
-
tarball 是 **115 个文件 / 约 1.
|
|
72
|
+
tarball 是 **115 个文件 / 约 1.02 MB**(`npm pack` 实测约 1,024,000 B,解包约 3.3 MB;再改文档会让字节数
|
|
69
73
|
小幅浮动,文件数不变),且 `package/packages/**` 为 **0**:根包不再内嵌
|
|
70
74
|
`packages/molbio-panel`,一个 tarball 里只有一个插件清单(面板包按自己的地址单独发布)。
|
|
71
75
|
面板本身没有因此消失——根包的 `lib/client.js` 就带着它,第 3.2 节的回读可以作证。
|
|
72
76
|
|
|
73
|
-
下面这次是在 **0.22.
|
|
77
|
+
下面这次是在 **0.22.2 的 tarball** 上、用 **dsh 0.2.1-rc.5** 的 CLI 重跑的(2026-10-30),
|
|
74
78
|
文件数与发布物一致;更早的记录只差版本号与文件数,流程未变。
|
|
75
79
|
|
|
76
80
|
### 3.1 安装
|
|
77
81
|
|
|
78
82
|
```
|
|
79
|
-
+ dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.22.
|
|
83
|
+
+ dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.22.2.tgz
|
|
80
84
|
Packages: +1
|
|
81
85
|
Done in 571ms
|
|
82
86
|
```
|
|
@@ -102,14 +106,19 @@ dsh web: http://127.0.0.1:50071/?token=...
|
|
|
102
106
|
```
|
|
103
107
|
|
|
104
108
|
读回结果:`HTTP 200`、38180 字节;页面里的 `window.__DSH_BOOT__` 图包含
|
|
105
|
-
`dsh-molbio-tools/client.js&rev
|
|
109
|
+
`dsh-molbio-tools/client.js&rev=…`(rev 是内容哈希,随产物变化),即浏览器半被宿主 Loader 发现并进了客户端图——
|
|
106
110
|
这是"启动成功"的可观察证据,而不只是进程没退出。(先用 token 换 cookie,再带 cookie 读 `/`,
|
|
107
111
|
否则第一跳是 303。)
|
|
108
112
|
|
|
113
|
+
再用同一 cookie POST `/api/agentPresets/list` 与 `/api/pluginInventory/list`(envelope 见
|
|
114
|
+
[rules.md](rules.md) 规则 1):`broken` 为空(空数组时服务端直接省略该键),molbio-lab 的
|
|
115
|
+
`tool-molbio` 为 `fiberPhase: "active"`——preset 里的 60 个工具**真的挂上了**,
|
|
116
|
+
而不只是组合树正确。
|
|
117
|
+
|
|
109
118
|
### 3.3 卸载
|
|
110
119
|
|
|
111
120
|
```
|
|
112
|
-
- dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.22.
|
|
121
|
+
- dsh-molbio-tools file:D:/.../dsh-molbio-tools-0.22.2.tgz
|
|
113
122
|
Done in 421ms
|
|
114
123
|
```
|
|
115
124
|
|
|
@@ -132,13 +141,15 @@ pnpm 的 hoisted linker 会在 `node_modules/` 留下一个指向仓库目录的
|
|
|
132
141
|
## 5. 仓库内验证(同一基线)
|
|
133
142
|
|
|
134
143
|
```
|
|
135
|
-
node test/contract.mjs →
|
|
136
|
-
node test/preset-health.mjs → all 30 rows load on dsh 0.2.1-rc.
|
|
144
|
+
node test/contract.mjs → 17 checks passed(host 契约、client 产物、tarball 覆盖、声明回退的突变自检)
|
|
145
|
+
node test/preset-health.mjs → all 30 rows load on dsh 0.2.1-rc.5
|
|
137
146
|
node test/smoke.mjs → registered 60 tools, all smoke tests passed
|
|
147
|
+
node test/drift-probe.mjs → 16/16(14 个突变全部被抓到)
|
|
148
|
+
node benchmark/run.mjs --offline → 61 tasks checked against the shipped tools
|
|
138
149
|
```
|
|
139
150
|
|
|
140
|
-
`test/release.mjs`
|
|
141
|
-
|
|
151
|
+
`test/release.mjs` 在完整权限的本地会话里整套通过(包括末尾的 npm 版本探测);
|
|
152
|
+
在受限沙箱里该探测会拿到 `EPERM`(Node 以管道捕获子进程输出被拒绝),属环境限制,不是插件问题。
|
|
142
153
|
|
|
143
154
|
## 6. 上架路径
|
|
144
155
|
|
package/docs/workflow.md
CHANGED
|
@@ -456,7 +456,7 @@ git status --short # lib/client.js 与 packages/molbio-panel/lib/clien
|
|
|
456
456
|
### DSH 升级后:preset 组合的维护(必做)
|
|
457
457
|
|
|
458
458
|
`preset/molbio-lab/agent.cordis.yml` 是官方 `standard` 预设的**行清单**副本 + 末尾一行
|
|
459
|
-
`tool-molbio`(当前基线:**dsh 0.2.1-rc.
|
|
459
|
+
`tool-molbio`(当前基线:**dsh 0.2.1-rc.5**)。它不会自动跟随 DSH 升级,因此每次升级
|
|
460
460
|
DSH 后:
|
|
461
461
|
|
|
462
462
|
1. 取新版的 shipped `standard`:
|
package/index.mjs
CHANGED
|
@@ -6914,15 +6914,46 @@ async function eutilsRequest(web, url, signal, label) {
|
|
|
6914
6914
|
}
|
|
6915
6915
|
}
|
|
6916
6916
|
|
|
6917
|
+
/** Credential-store references tried, in order, before the process environment. */
|
|
6918
|
+
const PUBMED_KEY_REFS = ['NCBI_API_KEY', 'MOLBIO_NCBI_API_KEY'];
|
|
6919
|
+
|
|
6920
|
+
/**
|
|
6921
|
+
* Resolve the NCBI API key from (1) the call argument, (2) the harness
|
|
6922
|
+
* credential store, (3) the process environment.
|
|
6923
|
+
*
|
|
6924
|
+
* The store is the recommended channel: a stored value is not injected into the
|
|
6925
|
+
* environment, so the agent's own shell children cannot read it by accident, and
|
|
6926
|
+
* `ctx.credentials.resolve` itself falls back through the launch environment and
|
|
6927
|
+
* the .env files. A provider that refuses this ref (a composition without a
|
|
6928
|
+
* credential store, a name outside its grammar) must degrade to the environment
|
|
6929
|
+
* rather than break the tool, hence the per-ref catch.
|
|
6930
|
+
*/
|
|
6931
|
+
async function pubmedKey(args, ctx) {
|
|
6932
|
+
const explicit = typeof args.api_key === 'string' ? args.api_key.trim() : '';
|
|
6933
|
+
if (explicit !== '') return explicit;
|
|
6934
|
+
const credentials = ctx.get('credentials');
|
|
6935
|
+
if (credentials !== undefined && typeof credentials.resolve === 'function') {
|
|
6936
|
+
for (const ref of PUBMED_KEY_REFS) {
|
|
6937
|
+
try {
|
|
6938
|
+
const hit = await credentials.resolve(ref);
|
|
6939
|
+
if (typeof hit?.value === 'string' && hit.value.trim() !== '') return hit.value.trim();
|
|
6940
|
+
} catch {
|
|
6941
|
+
/* fall through to the environment */
|
|
6942
|
+
}
|
|
6943
|
+
}
|
|
6944
|
+
}
|
|
6945
|
+
return pubmedApiKey(args);
|
|
6946
|
+
}
|
|
6947
|
+
|
|
6917
6948
|
const pubmedAbstractTool = (ctx) => define({
|
|
6918
6949
|
name: 'molbio_pubmed_abstract',
|
|
6919
|
-
description: 'Fetch PubMed abstracts for PMIDs via NCBI E-utilities (efetch). Requires the deployment to provide the web fetch capability; otherwise it reports that clearly.
|
|
6950
|
+
description: 'Fetch PubMed abstracts for PMIDs via NCBI E-utilities (efetch). Requires the deployment to provide the web fetch capability; otherwise it reports that clearly. Provide an NCBI API key through the harness credential store (NCBI_API_KEY), the api_key argument, or the environment to attribute the request to your NCBI key and raise the rate limit. Use it to pull abstracts for papers found by molbio_pubmed_search before adding them to the library.',
|
|
6920
6951
|
parameters: {
|
|
6921
6952
|
type: 'object',
|
|
6922
6953
|
required: ['pmids'],
|
|
6923
6954
|
properties: {
|
|
6924
6955
|
pmids: { type: 'array', items: { type: 'string' }, description: 'PubMed IDs, e.g. ["37607951"].' },
|
|
6925
|
-
api_key: { type: 'string', description: 'Optional NCBI API key for E-utilities.
|
|
6956
|
+
api_key: { type: 'string', description: 'Optional NCBI API key for E-utilities. Resolution order: this argument, the harness credential store (NCBI_API_KEY / MOLBIO_NCBI_API_KEY), then the environment. Prefer the credential store: an inline key appears in the conversation transcript.' },
|
|
6926
6957
|
},
|
|
6927
6958
|
},
|
|
6928
6959
|
outputSchema: {
|
|
@@ -6966,7 +6997,7 @@ const pubmedAbstractTool = (ctx) => define({
|
|
|
6966
6997
|
if (web === undefined || typeof web.fetch !== 'function') {
|
|
6967
6998
|
throw new MolbioInputError('this deployment provides no web fetch capability, so PubMed abstracts cannot be retrieved; molbio_pubmed_search still works for titles and snippets');
|
|
6968
6999
|
}
|
|
6969
|
-
const apiKey =
|
|
7000
|
+
const apiKey = await pubmedKey(args, ctx);
|
|
6970
7001
|
const url = pubmedFetchUrl(pmids, apiKey);
|
|
6971
7002
|
const result = await web.fetch({ url }, exec?.signal);
|
|
6972
7003
|
const body = result?.body?.content;
|
|
@@ -7318,14 +7349,14 @@ function paperServiceDeps(ctx, exec, args) {
|
|
|
7318
7349
|
|
|
7319
7350
|
const pubmedSearchTool = (ctx) => define({
|
|
7320
7351
|
name: 'molbio_pubmed_search',
|
|
7321
|
-
description: 'Search PubMed for literature. With an NCBI API key (the api_key argument,
|
|
7352
|
+
description: 'Search PubMed for literature. With an NCBI API key (the api_key argument, the harness credential store under NCBI_API_KEY / MOLBIO_NCBI_API_KEY, or the environment) it queries the official E-utilities API and returns PubMed records with PMIDs, titles, journal/year, authors and DOI; without a key it falls back to the harness web-search service. Add the papers you keep to the reading library with molbio_paper_add.',
|
|
7322
7353
|
parameters: {
|
|
7323
7354
|
type: 'object',
|
|
7324
7355
|
required: ['query'],
|
|
7325
7356
|
properties: {
|
|
7326
7357
|
query: requiredString('Search query, e.g. "CRISPR base editing review 2024" or "site:pubmed.ncbi.nlm.nih.gov KRAS G12D".'),
|
|
7327
7358
|
max_results: { type: 'integer', description: 'Maximum results (1-20, default 8).' },
|
|
7328
|
-
api_key: { type: 'string', description: 'Optional NCBI API key for the official PubMed E-utilities API.
|
|
7359
|
+
api_key: { type: 'string', description: 'Optional NCBI API key for the official PubMed E-utilities API. Resolution order: this argument, the harness credential store (NCBI_API_KEY / MOLBIO_NCBI_API_KEY), then the environment. Prefer the credential store: an inline key appears in the conversation transcript.' },
|
|
7329
7360
|
},
|
|
7330
7361
|
},
|
|
7331
7362
|
outputSchema: {
|
|
@@ -7378,7 +7409,7 @@ const pubmedSearchTool = (ctx) => define({
|
|
|
7378
7409
|
throw new MolbioInputError('max_results must be an integer between 1 and 20');
|
|
7379
7410
|
}
|
|
7380
7411
|
const web = ctx.get('web');
|
|
7381
|
-
const apiKey =
|
|
7412
|
+
const apiKey = await pubmedKey(args, ctx);
|
|
7382
7413
|
if (apiKey !== undefined) {
|
|
7383
7414
|
if (web === undefined || typeof web.fetch !== 'function') {
|
|
7384
7415
|
throw new MolbioInputError('an NCBI API key was provided, but this deployment provides no web fetch capability; the official PubMed API needs ctx.web.fetch');
|
|
@@ -7574,9 +7605,9 @@ const PROMPT_SECTION = `Molecular-biology tools (dsh-molbio-tools) are available
|
|
|
7574
7605
|
- Sequences & files: molbio_align (local alignment with a readable match line), molbio_msa_align (progressive multiple sequence alignment — pass an array of sequences or a workspace FASTA), molbio_conservation (consensus + per-column identity/conservation + variable positions; accepts aligned rows or raw sequences it aligns first), molbio_sequence_logo (draws the same alignment as a sequence logo SVG — pass the molbio_msa_align output rows as the alignment argument; letters are sized by per-position information content), molbio_fasta_fastq (FASTA/FASTQ stats/extract/convert/QC on workspace files), molbio_batch_analyze (run one summary/ORF/enzyme analysis over every matching FASTA/FASTQ/GenBank/.dna file in a directory and write one CSV/TSV table), molbio_extract_region (pull a CDS/promoter/region from a plasmid file by feature label or coordinates).
|
|
7575
7606
|
- CRISPR: molbio_grna_design (family-aware guide RNAs over a sequence or plasmid file: cas_family spcas9/sacas9/cas12a/cas13a/cas13b/cas13d selects the PAM side, crRNA length, seed and target type — Cas12a uses a 5' TTTV PAM and 23 nt crRNA; Cas13 targets RNA and reports a PFS preference rather than a PAM. Both strands are scanned for DNA families, the sense strand by default for Cas13; guides are filtered on GC/poly-T/self-complementarity and ranked, with a mismatch-tolerant off-target search over the same sequence — reaching a whole genome simply means passing it as the target). Pass the top candidates on for ordering with save_path (CSV) or map_path (SVG map); the ranking is a heuristic and says so, so present it as candidate ranking, never as a validated efficiency prediction.
|
|
7576
7607
|
- Map extras: pass gc_skew: true for the GC skew ring, show_unique_cutters: true to mark single-cutting enzymes on the map.
|
|
7577
|
-
- Literature & records: molbio_pubmed_abstract (fetch abstracts by PMID — needs web fetch;
|
|
7608
|
+
- Literature & records: molbio_pubmed_abstract (fetch abstracts by PMID — needs web fetch; an NCBI key comes from the api_key argument, the credential store's NCBI_API_KEY, or the environment), molbio_paper_export_bibtex (papers.json → .bib), molbio_protocol_add / molbio_protocol_list / molbio_protocol_update (protocols.json), molbio_experiment_log / molbio_experiment_list (experiments.json).
|
|
7578
7609
|
- Bench analysis (v19): molbio_fastq_qc (read-level QC for a FASTQ file: per-base quality with quartiles, per-read quality, base content, GC distribution, length, exact duplication, over-represented sequences, adapter screen, plus a report SVG — use it for "is this run good"), molbio_codon_usage (CAI/RSCU/Nc/GC3, rare-codon list and CDS CpG against E. coli/yeast/human — use it for "will this gene express"; it explains, molbio_codon_optimize rewrites), molbio_phylogenetic_tree (distance method: p-distance/JC/K2P/Tamura-Nei, UPGMA or neighbour-joining, bootstrap support, consensus, Newick file and an SVG tree — it is NOT a maximum-likelihood or Bayesian analysis, and bootstrap percentages are not p-values), molbio_pcr_simulate (where a primer pair amplifies: product sizes and coordinates on both strands, mismatch detail, the 3'-end exact-match requirement, circular templates, screening other templates, and an expected gel — use it for "what band will I see"), molbio_gc_composition (sliding-window GC, CpG islands under the Gardiner-Garden or Takai criteria, GC/AT skew with the cumulative-skew origin/terminus indication, dinucleotide obs/exp, entropy, complexity, N50/L50, plus a figure — use it for "is this a promoter or an origin").
|
|
7579
|
-
- Literature: molbio_pubmed_search — with an NCBI API key (api_key argument,
|
|
7610
|
+
- Literature: molbio_pubmed_search — with an NCBI API key (api_key argument, the credential store under NCBI_API_KEY / MOLBIO_NCBI_API_KEY, or the environment) it uses the official PubMed E-utilities API and returns PMIDs, titles, journal/year, authors and DOI; without a key it falls back to the harness web search. The reading library is molbio_paper_add / molbio_paper_list / molbio_paper_update / molbio_paper_remove (papers.json in the workspace).
|
|
7580
7611
|
|
|
7581
7612
|
Cloning workflow rules (follow them to avoid slow re-derivation):
|
|
7582
7613
|
- To clone a sequence into a plasmid, call molbio_clone_simulate FIRST — pass the BARE insert with add_flanks: true and the tool adds the enzyme recognition sites itself. The tool also validates single cutting, insert orientation, and internal sites itself: never pre-verify enzymes, cutting counts, or flank construction by thinking. Its output already contains final_sequence AND the remapped feature coordinates (features field). NEVER recompute feature coordinates or insert positions by reasoning — always take them from the tool output.
|
package/lib/client.js
CHANGED
|
@@ -8517,6 +8517,44 @@ window.__ModuleLoader__.load({
|
|
|
8517
8517
|
return undefined;
|
|
8518
8518
|
}
|
|
8519
8519
|
|
|
8520
|
+
/** The credential reference the PubMed tools read first. */
|
|
8521
|
+
const NCBI_KEY_REF = 'NCBI_API_KEY';
|
|
8522
|
+
|
|
8523
|
+
/**
|
|
8524
|
+
* Whether a Plugins-page detail subject is this package's own entry.
|
|
8525
|
+
*
|
|
8526
|
+
* Bundles and rows both expose `pkg.name`; the key section is offered on the
|
|
8527
|
+
* bundle page and on every row page the bundle declares, and on nothing else.
|
|
8528
|
+
*/
|
|
8529
|
+
function isMolbioDetailSubject(subject) {
|
|
8530
|
+
if (subject === null || typeof subject !== 'object') return false;
|
|
8531
|
+
if (subject.kind !== 'bundle' && subject.kind !== 'row') return false;
|
|
8532
|
+
const pkg = subject.pkg;
|
|
8533
|
+
return pkg !== null && typeof pkg === 'object' && pkg.name === 'dsh-molbio-tools';
|
|
8534
|
+
}
|
|
8535
|
+
|
|
8536
|
+
/**
|
|
8537
|
+
* The host failure message carried by a Remote result, or '' on success.
|
|
8538
|
+
* Remote calls answer `{ ok: true, value }` or `{ ok: false, error }`.
|
|
8539
|
+
*/
|
|
8540
|
+
function remoteFailure(response) {
|
|
8541
|
+
if (response === null || typeof response !== 'object' || response.ok !== false) return '';
|
|
8542
|
+
const message = response.error?.message;
|
|
8543
|
+
return typeof message === 'string' && message !== '' ? message : 'the host refused the request';
|
|
8544
|
+
}
|
|
8545
|
+
|
|
8546
|
+
/** One reference's view from a `credentials.describe` response (never the value). */
|
|
8547
|
+
function credentialInfo(response, ref) {
|
|
8548
|
+
const view = response !== null && typeof response === 'object' && response.ok !== false
|
|
8549
|
+
? response.value?.[ref]
|
|
8550
|
+
: undefined;
|
|
8551
|
+
return {
|
|
8552
|
+
configured: view?.configured === true,
|
|
8553
|
+
writable: view?.writable !== false,
|
|
8554
|
+
...typeof view?.source === 'string' && view.source !== '' ? { source: view.source } : {},
|
|
8555
|
+
};
|
|
8556
|
+
}
|
|
8557
|
+
|
|
8520
8558
|
exports.PLASMID_EXTENSIONS = PLASMID_EXTENSIONS;
|
|
8521
8559
|
exports.ALIGNMENT_EXTENSIONS = ALIGNMENT_EXTENSIONS;
|
|
8522
8560
|
exports.extensionOf = extensionOf;
|
|
@@ -8541,6 +8579,10 @@ window.__ModuleLoader__.load({
|
|
|
8541
8579
|
exports.paperFields = paperFields;
|
|
8542
8580
|
exports.paperSummary = paperSummary;
|
|
8543
8581
|
exports.paperLink = paperLink;
|
|
8582
|
+
exports.NCBI_KEY_REF = NCBI_KEY_REF;
|
|
8583
|
+
exports.isMolbioDetailSubject = isMolbioDetailSubject;
|
|
8584
|
+
exports.remoteFailure = remoteFailure;
|
|
8585
|
+
exports.credentialInfo = credentialInfo;
|
|
8544
8586
|
return exports;
|
|
8545
8587
|
};
|
|
8546
8588
|
__molbio_modules["build/client-entry.mjs"] = () => {
|
|
@@ -8577,7 +8619,7 @@ window.__ModuleLoader__.load({
|
|
|
8577
8619
|
* inside the factory would also have to be torn down by hand on unload.
|
|
8578
8620
|
*/
|
|
8579
8621
|
const { createElement: h, useEffect, useMemo, useRef, useState } = __ext0;
|
|
8580
|
-
const { LIBRARY_FILE, childPath, classifyEntry, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
|
|
8622
|
+
const { LIBRARY_FILE, NCBI_KEY_REF, childPath, classifyEntry, credentialInfo, isMolbioDetailSubject, remoteFailure, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
|
|
8581
8623
|
/** This implementation's identity, and the key its body/title register under. */
|
|
8582
8624
|
const PANEL_ID = 'dsh-molbio-tools';
|
|
8583
8625
|
/** Type discriminator the tab is opened by. */
|
|
@@ -9015,6 +9057,142 @@ window.__ModuleLoader__.load({
|
|
|
9015
9057
|
h('div', { style: styles.main }, ...detail));
|
|
9016
9058
|
}
|
|
9017
9059
|
|
|
9060
|
+
/** The state of the PubMed key, read from the host's credential store. */
|
|
9061
|
+
async function readNcbiKey(remote) {
|
|
9062
|
+
if (remote?.credentials === undefined || typeof remote.credentials.describe !== 'function') {
|
|
9063
|
+
return { phase: 'error', configured: false, writable: false, message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' };
|
|
9064
|
+
}
|
|
9065
|
+
let response;
|
|
9066
|
+
try {
|
|
9067
|
+
response = await remote.credentials.describe([NCBI_KEY_REF]);
|
|
9068
|
+
} catch (error) {
|
|
9069
|
+
response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
|
|
9070
|
+
}
|
|
9071
|
+
const failure = remoteFailure(response);
|
|
9072
|
+
if (failure !== '') return { phase: 'error', configured: false, writable: false, message: failure };
|
|
9073
|
+
return { phase: 'ready', ...credentialInfo(response, NCBI_KEY_REF), message: '' };
|
|
9074
|
+
}
|
|
9075
|
+
|
|
9076
|
+
const keyStyles = {
|
|
9077
|
+
section: { borderTop: '1px solid rgba(127, 127, 127, 0.28)', marginTop: 16, paddingTop: 12, display: 'grid', gap: 8 },
|
|
9078
|
+
title: { margin: 0, fontSize: 14 },
|
|
9079
|
+
note: { margin: 0, fontSize: 12, opacity: 0.75, lineHeight: 1.5 },
|
|
9080
|
+
status: { margin: 0, fontSize: 12 },
|
|
9081
|
+
row: { display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' },
|
|
9082
|
+
input: { flex: '1 1 240px', minWidth: 200, padding: '4px 8px' },
|
|
9083
|
+
message: { margin: 0, fontSize: 12 },
|
|
9084
|
+
};
|
|
9085
|
+
|
|
9086
|
+
/**
|
|
9087
|
+
* The Plugins → installed plugin → detail section that stores the NCBI key.
|
|
9088
|
+
*
|
|
9089
|
+
* It renders only for this package's own subject (bundle page, or one of the
|
|
9090
|
+
* row pages the bundle declares) and returns null everywhere else, which is the
|
|
9091
|
+
* contract `plugins.detail.section` asks a contribution to honour. The value is
|
|
9092
|
+
* write-only from here: `credentials.describe` answers whether a key is
|
|
9093
|
+
* configured and where it came from, never the key itself.
|
|
9094
|
+
*/
|
|
9095
|
+
function NcbiKeySection({ subject, remote }) {
|
|
9096
|
+
const [draft, setDraft] = useState('');
|
|
9097
|
+
const [state, setState] = useState({ phase: 'loading', configured: false, writable: true, message: '' });
|
|
9098
|
+
const subjectToken = isMolbioDetailSubject(subject)
|
|
9099
|
+
? `${subject.kind}:${subject.pkg.name}:${subject.row?.rowId ?? ''}`
|
|
9100
|
+
: '';
|
|
9101
|
+
useEffect(() => {
|
|
9102
|
+
if (subjectToken === '') return undefined;
|
|
9103
|
+
let live = true;
|
|
9104
|
+
const load = () => readNcbiKey(remote).then((next) => { if (live) setState(next); });
|
|
9105
|
+
load();
|
|
9106
|
+
// Another surface may rotate or clear the same reference (Settings, the
|
|
9107
|
+
// file watcher); follow the host event instead of showing a stale badge.
|
|
9108
|
+
const off = typeof remote?.$on === 'function'
|
|
9109
|
+
? remote.$on('credentials/reference-updated', (ref) => { if (ref === NCBI_KEY_REF) load(); })
|
|
9110
|
+
: undefined;
|
|
9111
|
+
return () => {
|
|
9112
|
+
live = false;
|
|
9113
|
+
if (typeof off === 'function') off();
|
|
9114
|
+
};
|
|
9115
|
+
}, [remote, subjectToken]);
|
|
9116
|
+
if (!isMolbioDetailSubject(subject)) return null;
|
|
9117
|
+
const busy = state.phase === 'saving';
|
|
9118
|
+
const save = async () => {
|
|
9119
|
+
const value = draft.trim();
|
|
9120
|
+
if (value === '') return;
|
|
9121
|
+
if (remote?.credentials === undefined || typeof remote.credentials.set !== 'function') {
|
|
9122
|
+
setState((previous) => ({ ...previous, phase: 'error', message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' }));
|
|
9123
|
+
return;
|
|
9124
|
+
}
|
|
9125
|
+
setState((previous) => ({ ...previous, phase: 'saving', message: '' }));
|
|
9126
|
+
let response;
|
|
9127
|
+
try {
|
|
9128
|
+
response = await remote.credentials.set(NCBI_KEY_REF, value);
|
|
9129
|
+
} catch (error) {
|
|
9130
|
+
response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
|
|
9131
|
+
}
|
|
9132
|
+
const failure = remoteFailure(response);
|
|
9133
|
+
setDraft('');
|
|
9134
|
+
const next = await readNcbiKey(remote);
|
|
9135
|
+
setState(failure !== ''
|
|
9136
|
+
? { ...next, phase: 'error', message: failure }
|
|
9137
|
+
: next.phase === 'error'
|
|
9138
|
+
? next
|
|
9139
|
+
: next.configured === true
|
|
9140
|
+
? { ...next, message: 'Saved. The next PubMed call uses this key.' }
|
|
9141
|
+
: { ...next, phase: 'error', message: 'The host accepted the write but still reports no key configured. Restart the profile and try again.' });
|
|
9142
|
+
};
|
|
9143
|
+
const remove = async () => {
|
|
9144
|
+
if (remote?.credentials === undefined || typeof remote.credentials.unset !== 'function') {
|
|
9145
|
+
setState((previous) => ({ ...previous, phase: 'error', message: 'This deployment exposes no credentials Remote (the settings controller is not mounted).' }));
|
|
9146
|
+
return;
|
|
9147
|
+
}
|
|
9148
|
+
setState((previous) => ({ ...previous, phase: 'saving', message: '' }));
|
|
9149
|
+
let response;
|
|
9150
|
+
try {
|
|
9151
|
+
response = await remote.credentials.unset(NCBI_KEY_REF);
|
|
9152
|
+
} catch (error) {
|
|
9153
|
+
response = { ok: false, error: { message: error instanceof Error ? error.message : String(error) } };
|
|
9154
|
+
}
|
|
9155
|
+
const failure = remoteFailure(response);
|
|
9156
|
+
const next = await readNcbiKey(remote);
|
|
9157
|
+
setState(failure !== ''
|
|
9158
|
+
? { ...next, phase: 'error', message: failure }
|
|
9159
|
+
: next.phase === 'error'
|
|
9160
|
+
? next
|
|
9161
|
+
: next.configured === false
|
|
9162
|
+
? { ...next, message: 'Removed. PubMed search falls back to web search.' }
|
|
9163
|
+
: { ...next, phase: 'error', message: 'The host still reports a key configured after Remove.' });
|
|
9164
|
+
};
|
|
9165
|
+
const status = state.phase === 'loading'
|
|
9166
|
+
? 'Checking the credential store…'
|
|
9167
|
+
: state.phase === 'error'
|
|
9168
|
+
? 'Unavailable'
|
|
9169
|
+
: state.configured
|
|
9170
|
+
? `Configured${state.source === undefined ? '' : ` (source: ${state.source})`}`
|
|
9171
|
+
: 'Not configured';
|
|
9172
|
+
return h('section', { 'data-molbio-ncbi-key': '', style: keyStyles.section },
|
|
9173
|
+
h('h4', { style: keyStyles.title }, 'PubMed API key'),
|
|
9174
|
+
h('p', { style: keyStyles.note },
|
|
9175
|
+
'Stored in the harness credential store. molbio_pubmed_search and molbio_pubmed_abstract use it for the official PubMed E-utilities API; without it they fall back to web search.'),
|
|
9176
|
+
h('p', { 'data-molbio-key-status': state.phase, style: keyStyles.status }, status),
|
|
9177
|
+
h('div', { style: keyStyles.row },
|
|
9178
|
+
h('input', {
|
|
9179
|
+
type: 'password',
|
|
9180
|
+
value: draft,
|
|
9181
|
+
placeholder: 'NCBI API key (36 characters)',
|
|
9182
|
+
autoComplete: 'off',
|
|
9183
|
+
spellCheck: false,
|
|
9184
|
+
'aria-label': 'NCBI API key',
|
|
9185
|
+
style: keyStyles.input,
|
|
9186
|
+
onChange: (event) => setDraft(event.target.value),
|
|
9187
|
+
}),
|
|
9188
|
+
h('button', { type: 'button', disabled: busy || state.writable === false || draft.trim() === '', onClick: save },
|
|
9189
|
+
busy ? 'Saving…' : 'Save'),
|
|
9190
|
+
h('button', { type: 'button', disabled: busy || state.writable === false || state.configured !== true, onClick: remove }, 'Remove')),
|
|
9191
|
+
state.message === '' ? null : h('p', { 'data-molbio-key-message': '', role: 'status', style: keyStyles.message }, state.message),
|
|
9192
|
+
h('p', { style: keyStyles.note },
|
|
9193
|
+
'Create a key under NCBI → Account Settings → API Key Management. Prefer this over an environment variable: the store is not inherited by shell commands.'));
|
|
9194
|
+
}
|
|
9195
|
+
|
|
9018
9196
|
/** Required browser services. */
|
|
9019
9197
|
const inject = ['slots', 'sidebarRightTabs', 'remote'];
|
|
9020
9198
|
|
|
@@ -9098,6 +9276,25 @@ window.__ModuleLoader__.load({
|
|
|
9098
9276
|
key: toolName,
|
|
9099
9277
|
}, MolbioMapCard)), `molbio panel: ${toolName} card`);
|
|
9100
9278
|
}
|
|
9279
|
+
// Plugins -> the installed plugin's detail page: the one place a user can
|
|
9280
|
+
// store the NCBI key without editing a YAML file or exporting a variable.
|
|
9281
|
+
//
|
|
9282
|
+
// `remote.credentials` is a FIBER INJECTION, not merely a property on the
|
|
9283
|
+
// Remote carrier: the shipped settings pages declare it in `inject` for the
|
|
9284
|
+
// same reason. Without the declaration `ctx.remote.credentials` is undefined,
|
|
9285
|
+
// which is exactly how the first build of this section made Save a no-op.
|
|
9286
|
+
// The gate keeps the panel working in deployments that expose no credentials
|
|
9287
|
+
// namespace: the section simply never registers there.
|
|
9288
|
+
ctx.effect(() => {
|
|
9289
|
+
const gate = ctx.inject(['remote.credentials'], (keyCtx) => {
|
|
9290
|
+
keyCtx.effect(() => keyCtx.slots.inject('plugins.detail.section', () => keyCtx.slots.register({
|
|
9291
|
+
name: 'plugins.detail.section',
|
|
9292
|
+
id: 'molbio-ncbi-key',
|
|
9293
|
+
order: 100,
|
|
9294
|
+
}, (props) => h(NcbiKeySection, { ...props, remote: keyCtx.remote }))), 'molbio panel: NCBI key section');
|
|
9295
|
+
});
|
|
9296
|
+
return () => { gate.dispose(); };
|
|
9297
|
+
}, 'molbio panel: NCBI key section gate');
|
|
9101
9298
|
}
|
|
9102
9299
|
|
|
9103
9300
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-molbio-tools",
|
|
3
|
-
"version": "0.22.
|
|
3
|
+
"version": "0.22.2",
|
|
4
4
|
"description": "Zero-dependency molecular-biology plugin bundle for the DeepSeek Harness: sequence analysis, primer design (incl. cross-intron qPCR primers, TaqMan hydrolysis/MGB probes with ordering CSV and multiplex compatibility), in-silico PCR, cloning simulation, SnapGene .dna parsing and plasmid maps, Sanger verification, protein tools (incl. helical wheel and hydropathy plots), methylation-aware restriction analysis with double-digest buffer checks, qPCR analysis and plotting, multiple sequence alignment that conserves every input residue, conservation analysis, sequence logos, codon-usage analysis, phylogenetic trees with bootstrap support, Newick I/O and long-name label wrapping, CpG-island/GC-skew composition analysis, read-level FASTQ QC, batch file analysis with CSV export, silent restriction-site mutagenesis, family-aware CRISPR gRNA design (SpCas9/SaCas9/Cas12a/Cas13) with off-target search, an opt-in hand-off that shows a generated plot to the model as an attached image, plus a browser-side right-sidebar panel (plasmid maps and sequence logos drawn in the Web GUI) and a literature/protocol/experiment record system.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=24",
|
|
9
9
|
"dsh": ">=0.2.1-rc.2 <0.3.0-0"
|
|
10
10
|
},
|
|
11
11
|
"exports": {
|
|
@@ -96,7 +96,9 @@
|
|
|
96
96
|
"dshReleases": {
|
|
97
97
|
"0.2.1-rc.1": "unknown",
|
|
98
98
|
"0.2.1-rc.2": "compatible",
|
|
99
|
-
"0.2.1-rc.3": "compatible"
|
|
99
|
+
"0.2.1-rc.3": "compatible",
|
|
100
|
+
"0.2.1-rc.4": "unknown",
|
|
101
|
+
"0.2.1-rc.5": "compatible"
|
|
100
102
|
}
|
|
101
103
|
},
|
|
102
104
|
"bundle": {
|
package/test/client.mjs
CHANGED
|
@@ -101,6 +101,7 @@ assert.deepEqual([...module.inject], ['slots', 'sidebarRightTabs', 'remote'], 't
|
|
|
101
101
|
|
|
102
102
|
const effects = [];
|
|
103
103
|
const tabTypes = [];
|
|
104
|
+
const injectedDeps = [];
|
|
104
105
|
const slots = createSlotsStub();
|
|
105
106
|
const ctx = {
|
|
106
107
|
remote: {
|
|
@@ -122,9 +123,16 @@ const ctx = {
|
|
|
122
123
|
effects.push(label);
|
|
123
124
|
return factory();
|
|
124
125
|
},
|
|
126
|
+
inject(deps, callback) {
|
|
127
|
+
injectedDeps.push([...deps]);
|
|
128
|
+
const child = Object.create(ctx);
|
|
129
|
+
child.remote = { ...ctx.remote, credentials: {} };
|
|
130
|
+
return { dispose() {}, value: callback(child) };
|
|
131
|
+
},
|
|
125
132
|
sidebarRightTabs: { register(definition) { tabTypes.push(definition); return () => undefined; } },
|
|
126
133
|
slots,
|
|
127
134
|
};
|
|
135
|
+
assert.deepEqual(injectedDeps, [], 'no dependency gate has run yet');
|
|
128
136
|
assert.doesNotThrow(() => module.apply(ctx), 'apply() must not require any seat to be declared yet');
|
|
129
137
|
|
|
130
138
|
assert.deepEqual(effects, [
|
|
@@ -136,7 +144,10 @@ assert.deepEqual(effects, [
|
|
|
136
144
|
'molbio panel: papers tab title',
|
|
137
145
|
'molbio panel: molbio_plasmid_map card',
|
|
138
146
|
'molbio panel: molbio_plasmid_map_file card',
|
|
147
|
+
'molbio panel: NCBI key section gate',
|
|
148
|
+
'molbio panel: NCBI key section',
|
|
139
149
|
]);
|
|
150
|
+
assert.deepEqual(injectedDeps, [['remote.credentials']], 'the key section waits for the credentials Remote namespace');
|
|
140
151
|
assert.equal(tabTypes.length, 2, 'the package contributes two page tab types');
|
|
141
152
|
assert.deepEqual(
|
|
142
153
|
tabTypes.map((type) => [type.id, type.kind, type.title()]),
|
|
@@ -153,7 +164,7 @@ assert.deepEqual(tabTypes.map((type) => type.guide[0].order), [40, 41], 'the gui
|
|
|
153
164
|
assert.equal(slots.registrations.length, 0, 'nothing registers before its seat exists');
|
|
154
165
|
assert.deepEqual(
|
|
155
166
|
slots.injectedSeats,
|
|
156
|
-
['sidebar.right.pane.tab', 'sidebar.right.pane.tab.title', 'tool.call.toolview'],
|
|
167
|
+
['sidebar.right.pane.tab', 'sidebar.right.pane.tab.title', 'tool.call.toolview', 'plugins.detail.section'],
|
|
157
168
|
'each seat is claimed by waiting for its declaration, not by registering blindly',
|
|
158
169
|
);
|
|
159
170
|
// The shell's guard, reproduced on the stub: this is the exact exception a bare
|
|
@@ -169,8 +180,8 @@ slots.declare('sidebar.right.pane.tab');
|
|
|
169
180
|
slots.declare('sidebar.right.pane.tab.title');
|
|
170
181
|
assert.deepEqual(
|
|
171
182
|
slots.pendingSeats,
|
|
172
|
-
['tool.call.toolview'],
|
|
173
|
-
'the transcript card
|
|
183
|
+
['tool.call.toolview', 'plugins.detail.section'],
|
|
184
|
+
'the transcript card and the plugin-detail section wait for the seats their owners declare',
|
|
174
185
|
);
|
|
175
186
|
assert.equal(slots.registrations.length, 4, 'both tab bodies and titles land as soon as their seat exists');
|
|
176
187
|
slots.declare('tool.call.toolview');
|
|
@@ -211,6 +222,39 @@ assert.equal(claim('tool.call.toolview', 'molbio_plasmid_map').registration.inje
|
|
|
211
222
|
slots.declare('tool.call.toolview');
|
|
212
223
|
assert.equal(slots.registrations.length, 6, 're-declaring the seat re-installs both cards instead of duplicating them');
|
|
213
224
|
|
|
225
|
+
// The Plugins page's detail seat belongs to the plugin manager. The section
|
|
226
|
+
// lands when that seat is declared, and the component itself decides whether
|
|
227
|
+
// the open subject is this package.
|
|
228
|
+
slots.declare('plugins.detail.section');
|
|
229
|
+
assert.equal(slots.registrations.length, 7, 'the key section lands when the plugin manager declares its seat');
|
|
230
|
+
const keyClaim = slots.registrations.find((entry) => entry.registration.name === 'plugins.detail.section');
|
|
231
|
+
assert.ok(keyClaim, 'the key section is claimed in plugins.detail.section');
|
|
232
|
+
assert.equal(keyClaim.registration.id, 'molbio-ncbi-key', 'the list seat is addressed by an id of our own');
|
|
233
|
+
assert.equal(typeof keyClaim.component, 'function', 'the section is a component');
|
|
234
|
+
// The registered component binds `remote`; invoking its element type is how a
|
|
235
|
+
// test renders it without React (the hooks are stubbed by this file's reactStub).
|
|
236
|
+
const ours = keyClaim.component({ subject: { kind: 'bundle', pkg: { name: 'dsh-molbio-tools' } } });
|
|
237
|
+
assert.equal(typeof ours.type, 'function', 'the wrapper renders this package\'s section');
|
|
238
|
+
const sectionTree = ours.type(ours.props);
|
|
239
|
+
assert.equal(sectionTree.type, 'section');
|
|
240
|
+
assert.equal(sectionTree.props['data-molbio-ncbi-key'], '');
|
|
241
|
+
const flat = [];
|
|
242
|
+
const walk = (node) => {
|
|
243
|
+
if (node === null || typeof node !== 'object') return;
|
|
244
|
+
flat.push(node);
|
|
245
|
+
for (const child of Array.isArray(node.children) ? node.children : []) walk(child);
|
|
246
|
+
};
|
|
247
|
+
walk(sectionTree);
|
|
248
|
+
assert.ok(flat.some((node) => node.type === 'h4' && node.children.join('') === 'PubMed API key'), 'the section is titled');
|
|
249
|
+
assert.ok(flat.some((node) => node.type === 'input' && node.props.type === 'password'), 'the section offers a password input');
|
|
250
|
+
assert.ok(flat.some((node) => node.type === 'button' && node.children.join('') === 'Save'), 'the section offers a save control');
|
|
251
|
+
const renderFor = (subject) => {
|
|
252
|
+
const wrapper = keyClaim.component({ subject });
|
|
253
|
+
return wrapper.type(wrapper.props);
|
|
254
|
+
};
|
|
255
|
+
assert.equal(renderFor({ kind: 'bundle', pkg: { name: 'another-plugin' } }), null, 'another package gets no section');
|
|
256
|
+
assert.equal(renderFor({ kind: 'item', id: 'web-search' }), null, 'an official-plugin page gets no section');
|
|
257
|
+
|
|
214
258
|
// ── 3. the panel's data path, against the real fixture ──────────────────────
|
|
215
259
|
|
|
216
260
|
assert.equal(panelCore.extensionOf('pUC118.dna'), 'dna');
|
|
@@ -231,6 +275,21 @@ const sorted = panelCore.sortEntries([
|
|
|
231
275
|
]);
|
|
232
276
|
assert.deepEqual(sorted.map((entry) => entry.name), ['a.dna', 'z.dna', 'a.fasta', 'notes.md']);
|
|
233
277
|
|
|
278
|
+
// The plugin-detail key section's pure helpers: which subject it belongs to,
|
|
279
|
+
// how a credential view is read, and how a refused Remote call is surfaced.
|
|
280
|
+
assert.equal(panelCore.isMolbioDetailSubject({ kind: 'bundle', pkg: { name: 'dsh-molbio-tools' } }), true);
|
|
281
|
+
assert.equal(panelCore.isMolbioDetailSubject({ kind: 'row', pkg: { name: 'dsh-molbio-tools' }, row: { rowId: 'molbio-client' } }), true);
|
|
282
|
+
assert.equal(panelCore.isMolbioDetailSubject({ kind: 'bundle', pkg: { name: 'another-plugin' } }), false);
|
|
283
|
+
assert.equal(panelCore.isMolbioDetailSubject({ kind: 'item', id: 'dsh-molbio-tools' }), false, 'official-plugin pages are not ours');
|
|
284
|
+
assert.equal(panelCore.isMolbioDetailSubject(null), false);
|
|
285
|
+
assert.deepEqual(
|
|
286
|
+
panelCore.credentialInfo({ ok: true, value: { NCBI_API_KEY: { configured: true, source: 'file', writable: true } } }, 'NCBI_API_KEY'),
|
|
287
|
+
{ configured: true, writable: true, source: 'file' },
|
|
288
|
+
);
|
|
289
|
+
assert.deepEqual(panelCore.credentialInfo({ ok: true, value: {} }, 'NCBI_API_KEY'), { configured: false, writable: true });
|
|
290
|
+
assert.equal(panelCore.remoteFailure({ ok: false, error: { message: 'credential/rejected: the store is unreadable' } }), 'credential/rejected: the store is unreadable');
|
|
291
|
+
assert.equal(panelCore.remoteFailure({ ok: true, value: {} }), '', 'a successful result carries no failure');
|
|
292
|
+
|
|
234
293
|
assert.equal(panelCore.childPath('D:\\work', 'a.dna'), 'D:\\work/a.dna');
|
|
235
294
|
assert.equal(panelCore.childPath('', 'a.dna'), 'a.dna');
|
|
236
295
|
|