@mzzsfy/dsh-shell-select 0.4.1 → 0.4.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,120 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { Context } from '@deepseek-ai/cordis'
4
+ import { createScope } from '@deepseek-ai/dsh-scope'
5
+ import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
6
+ import ToolRuntime, { defineTool, renderToolsSdk, renderToolsSdkPy } from '@deepseek-ai/dsh-tools'
7
+ import * as projection from '../src/prompt-projection.mjs'
8
+
9
+ function fixture() {
10
+ const ctx = new Context()
11
+ new SystemPrompt(ctx, {})
12
+ new ToolRuntime(ctx, {})
13
+ const preset = {}
14
+ const scoped = createScope(ctx, preset)
15
+ const register = (target, name, description) => target.tools.register(defineTool({
16
+ name, description, parameters: {},
17
+ output: { schema: { type: 'string' }, render: (value) => value },
18
+ execute: async () => 'PowerShell',
19
+ }))
20
+ register(ctx, 'shell', 'multi client')
21
+ register(ctx, 'pwsh', 'Do not call; use shell.')
22
+ register(scoped.ctx, 'pwsh', 'official preset description')
23
+ scoped.ctx.systemPrompt.section({ name: 'tool:pwsh', order: 1001, text: 'duplicate guidance' })
24
+ ctx.systemPrompt.section({ name: 'tool:shell', order: 1000, text: 'shell guidance' })
25
+ ctx.systemPrompt.context({ name: 'retained-context', order: 1, text: 'cwd' })
26
+ return { ctx, scoped, preset }
27
+ }
28
+
29
+ // Given 官方预设遮蔽根描述;When 组装;Then 无pwsh展示但历史调用保持可执行。
30
+ test('native 投影穿过预设遮蔽且不影响调用', async () => {
31
+ const { ctx, preset, scoped } = fixture()
32
+ const agent = {}
33
+ const child = createScope(ctx, agent, { parent: preset })
34
+ const original = await ctx.systemPrompt.assemble({ scope: agent })
35
+ assert.equal(original.tools.find((t) => t.name === 'pwsh').description, 'official preset description')
36
+ projection.mountShellPromptProjection(ctx)
37
+ const result = await ctx.systemPrompt.assemble({ scope: agent })
38
+ assert.deepEqual(result.tools.map((t) => t.name), ['shell'])
39
+ assert.deepEqual(result.sections.filter((s) => s.name.startsWith('tool:')).map((s) => s.name), ['tool:shell'])
40
+ assert.deepEqual(result.contexts, original.contexts)
41
+ assert.deepEqual(result.variables, original.variables)
42
+ assert.equal(ctx.tools.get('pwsh', agent).description, 'official preset description')
43
+ assert.equal(await ctx.tools.get('pwsh', agent).execute({}, {}), 'PowerShell')
44
+ await child.dispose()
45
+ await scoped.dispose()
46
+ })
47
+
48
+ // Given 监听卸载;When 再次组装;Then 原预设schema与提示段恢复。
49
+ test('投影卸载自然还原', async () => {
50
+ const { ctx, preset, scoped } = fixture()
51
+ const original = await ctx.systemPrompt.assemble({ scope: preset })
52
+ const dispose = projection.mountShellPromptProjection(ctx)
53
+ await ctx.systemPrompt.assemble({ scope: preset })
54
+ dispose()
55
+ assert.deepEqual(await ctx.systemPrompt.assemble({ scope: preset }), original)
56
+ await scoped.dispose()
57
+ })
58
+
59
+ // Given 子代理排除shell;When 组装;Then 不移除其唯一终端。
60
+ test('shell 不可见时保留 pwsh', async () => {
61
+ const { ctx, preset, scoped } = fixture()
62
+ const agent = {}
63
+ const child = createScope(ctx, agent, { parent: preset })
64
+ child.ctx.tools.restrict({ deny: ['shell'] })
65
+ const original = await ctx.systemPrompt.assemble({ scope: agent })
66
+ projection.mountShellPromptProjection(ctx)
67
+ assert.deepEqual(await ctx.systemPrompt.assemble({ scope: agent }), original)
68
+ await child.dispose()
69
+ await scoped.dispose()
70
+ })
71
+
72
+ for (const [language, render] of [['typescript', renderToolsSdk], ['python', renderToolsSdkPy]]) {
73
+ for (const mode of ['ptc', 'both']) {
74
+ // Given PTC SDK;When 组装;Then 仅删除pwsh声明,其他工具类型不变。
75
+ test(`${language}/${mode} SDK 删除 pwsh 且保留其他输出类型`, async () => {
76
+ const { ctx, preset, scoped } = fixture()
77
+ const schemas = ctx.tools.schemas(preset).map((s) => ({ ...s, output: ctx.tools.get(s.name, preset).output.schema }))
78
+ ctx.on('system-prompt/assemble', async (assembly, context, next) => {
79
+ const result = await next()
80
+ return { ...result, tools: mode === 'ptc' ? [{ name: 'run_code' }] : [...result.tools, { name: 'run_code' }],
81
+ sections: [...result.sections, { name: 'tools:sdk', text: render(schemas), interpolate: false }] }
82
+ })
83
+ ctx.reflect.provide('ptcRuntime', { language })
84
+ projection.mountShellPromptProjection(ctx)
85
+ const result = await ctx.systemPrompt.assemble({ scope: preset })
86
+ assert.deepEqual(result.tools.map((t) => t.name), mode === 'ptc' ? ['run_code'] : ['shell', 'run_code'])
87
+ assert.equal(result.sections.find((s) => s.name === 'tools:sdk').text, render(schemas.filter((s) => s.name !== 'pwsh')))
88
+ assert.equal(await ctx.tools.get('pwsh', preset).execute({}, {}), 'PowerShell')
89
+ await scoped.dispose()
90
+ })
91
+ }
92
+ }
93
+
94
+ // Given 其他waterfall在next中添加pwsh;When投影;Then最终结果过滤而非入口快照。
95
+ test('投影过滤 next 返回的最终结果且不篡改输入', async () => {
96
+ const handlers = []
97
+ const warnings = []
98
+ const ctx = { on: (name, handler) => { handlers.push(handler); return () => {} }, tools: { get: () => ({}) }, logger: { warn: (msg) => warnings.push(msg) } }
99
+ projection.mountShellPromptProjection(ctx)
100
+ const input = { tools: [], sections: [] }
101
+ const final = { tools: [{ name: 'shell' }, { name: 'pwsh' }, { name: 'read' }], sections: [{ name: 'tool:pwsh' }, { name: 'policy' }], contexts: [], variables: {} }
102
+ const result = await handlers[0](input, {}, async () => final)
103
+ assert.deepEqual(result.tools.map((t) => t.name), ['shell', 'read'])
104
+ assert.deepEqual(result.sections.map((s) => s.name), ['policy'])
105
+ assert.equal(final.tools.length, 3)
106
+ assert.equal(input.tools.length, 0)
107
+ assert.deepEqual(warnings, [])
108
+ })
109
+
110
+ // Given 无法识别SDK语言;When投影;Then原样返回且告警,不做半完成SDK。
111
+ test('SDK 投影不可用时整体不改并告警', async () => {
112
+ const handlers = []
113
+ const warnings = []
114
+ const ctx = { on: (name, handler) => { handlers.push(handler); return () => {} }, tools: { get: () => ({}) },
115
+ get: () => ({ language: 'unsupported' }), logger: { warn: (msg) => warnings.push(msg) } }
116
+ projection.mountShellPromptProjection(ctx)
117
+ const final = { tools: [{ name: 'shell' }, { name: 'pwsh' }, { name: 'run_code' }], sections: [{ name: 'tools:sdk', text: 'pwsh sdk' }] }
118
+ assert.equal(await handlers[0](final, {}, async () => final), final)
119
+ assert.equal(warnings.length, 1)
120
+ })
@@ -0,0 +1,41 @@
1
+ import { test } from 'node:test'
2
+ import assert from 'node:assert/strict'
3
+ import { mountPwshTakeover } from '../src/pwsh-takeover.mjs'
4
+
5
+ test('pwsh 接管事实仅归属实际进入执行器的并行调用', async () => {
6
+ // Given 两条 pwsh 调用;When 仅一条进入接管执行器;Then 仅该结果带标记,值与退出锚不变。
7
+ const listeners = new Map()
8
+ const touch = mountPwshTakeover({ on: (name, fn) => listeners.set(name, fn), effect: (fn) => fn() })
9
+ const dispatch = listeners.get('tools/execute')
10
+ const post = listeners.get('tools/post-execute')
11
+ const makeExec = () => ({ name: 'pwsh', token: Symbol() })
12
+ const taken = makeExec()
13
+ const untouched = makeExec()
14
+ const value = { exitCode: 2 }
15
+ const result = { isError: false, value, content: [{ type: 'text', text: 'out\n[exit code: 2]' }] }
16
+ await Promise.all([
17
+ dispatch(taken, async () => { await Promise.resolve(); touch(); return result }),
18
+ dispatch(untouched, async () => result),
19
+ ])
20
+ const marked = await post(taken, result, async () => ({ kind: 'accept' }))
21
+ assert.equal(marked.kind, 'accept')
22
+ assert.equal(marked.content[0].text, 'out\n[exit code: 2]')
23
+ assert.deepEqual(marked.content[1], { type: 'shell-select-takeover', tool: 'pwsh' })
24
+ assert.equal(result.value, value)
25
+ assert.deepEqual(await post(untouched, result, async () => ({ kind: 'accept' })), { kind: 'accept' })
26
+ })
27
+
28
+ test('普通 shell 与下游替换内容保持不变,接管事实消费一次', async () => {
29
+ // Given shell 与已接管 pwsh;When 下游策略替换内容;Then 不覆盖策略,调用事实无残留。
30
+ const listeners = new Map()
31
+ const touch = mountPwshTakeover({ on: (name, fn) => listeners.set(name, fn), effect: (fn) => fn() })
32
+ const result = { content: [{ type: 'text', text: 'out' }], isError: false }
33
+ const shell = { name: 'shell', token: Symbol() }
34
+ await listeners.get('tools/execute')(shell, async () => { touch(); return result })
35
+ assert.deepEqual(await listeners.get('tools/post-execute')(shell, result, async () => ({ kind: 'accept' })), { kind: 'accept' })
36
+ const pwsh = { name: 'pwsh', token: Symbol() }
37
+ await listeners.get('tools/execute')(pwsh, async () => { touch(); return result })
38
+ const replacement = { kind: 'accept', content: [{ type: 'text', text: 'policy replacement' }] }
39
+ assert.equal(await listeners.get('tools/post-execute')(pwsh, result, async () => replacement), replacement)
40
+ assert.deepEqual(await listeners.get('tools/post-execute')(pwsh, result, async () => ({ kind: 'accept' })), { kind: 'accept' })
41
+ })
@@ -25,7 +25,7 @@ const ENV_WITH_SYSTEM32_PATH = {
25
25
  }
26
26
 
27
27
  test('pwsh 候选顺序:PowerShell 7 → PATH 各项 → System32 5.1', () => {
28
- const candidates = candidatePaths('pwsh', ENV)
28
+ const candidates = candidatePaths('pwsh', ENV, 'win32')
29
29
  assert.deepEqual(candidates, [
30
30
  'C:\\PF\\PowerShell\\7\\pwsh.exe',
31
31
  'C:\\one\\pwsh.exe',
@@ -36,7 +36,7 @@ test('pwsh 候选顺序:PowerShell 7 → PATH 各项 → System32 5.1', () => {
36
36
  })
37
37
 
38
38
  test('bash 候选顺序:Git 三常见位置 → msys2 真实 bash → PATH', () => {
39
- const candidates = candidatePaths('bash', ENV)
39
+ const candidates = candidatePaths('bash', ENV, 'win32')
40
40
  assert.deepEqual(candidates, [
41
41
  'C:\\PF\\Git\\bin\\bash.exe',
42
42
  'C:\\PF86\\Git\\bin\\bash.exe',
@@ -50,7 +50,7 @@ test('bash 候选顺序:Git 三常见位置 → msys2 真实 bash → PATH', ()
50
50
  })
51
51
 
52
52
  test('bash 候选排除 SystemRoot 下的 PATH 条目(WSL forwarder)', () => {
53
- const candidates = candidatePaths('bash', ENV_WITH_SYSTEM32_PATH)
53
+ const candidates = candidatePaths('bash', ENV_WITH_SYSTEM32_PATH, 'win32')
54
54
  assert.deepEqual(candidates, [
55
55
  'C:\\PF\\Git\\bin\\bash.exe',
56
56
  'C:\\PF86\\Git\\bin\\bash.exe',
@@ -63,21 +63,21 @@ test('bash 候选排除 SystemRoot 下的 PATH 条目(WSL forwarder)', () => {
63
63
 
64
64
  test('bash 候选排除 SystemRoot 下的正斜杠 PATH 条目', () => {
65
65
  const env = { ...ENV, PATH: 'C:/WINDOWS/System32;C:\\one' }
66
- const candidates = candidatePaths('bash', env)
66
+ const candidates = candidatePaths('bash', env, 'win32')
67
67
  assert.ok(!candidates.some((candidate) => candidate.toLowerCase().startsWith('c:\\windows')))
68
68
  assert.ok(candidates.includes('C:\\one\\bash.exe'))
69
69
  })
70
70
 
71
71
  test('SystemRoot 带尾分隔符时排除规则仍命中', () => {
72
72
  const env = { ...ENV, SystemRoot: 'C:\\WINDOWS\\', PATH: 'C:\\WINDOWS\\System32;C:\\one' }
73
- const candidates = candidatePaths('bash', env)
73
+ const candidates = candidatePaths('bash', env, 'win32')
74
74
  assert.ok(!candidates.some((candidate) => candidate.toLowerCase().startsWith('c:\\windows')), '尾分隔符 SystemRoot 不得让 forwarder 入候选')
75
75
  assert.ok(candidates.includes('C:\\one\\bash.exe'))
76
76
  })
77
77
 
78
78
  test('LocalAppData 缺省:LAD 锚位跳过,其余候选不受影响', () => {
79
79
  const env = { ProgramFiles: 'C:\\PF', 'ProgramFiles(x86)': 'C:\\PF86', LocalAppData: '', PATH: '' }
80
- const candidates = candidatePaths('bash', env)
80
+ const candidates = candidatePaths('bash', env, 'win32')
81
81
  assert.ok(!candidates.some((candidate) => candidate.startsWith('\\\\')), 'LocalAppData 为空不得产出空根拼接候选')
82
82
  assert.ok(!candidates.some((candidate) => candidate.startsWith('undefined')))
83
83
  assert.ok(candidates.includes('C:\\PF\\Git\\bin\\bash.exe'))
@@ -85,14 +85,14 @@ test('LocalAppData 缺省:LAD 锚位跳过,其余候选不受影响', () => {
85
85
  })
86
86
 
87
87
  test('bash 候选永不含 msys2.exe 启动器(管道下静默失败)', () => {
88
- for (const candidate of candidatePaths('bash', ENV)) {
88
+ for (const candidate of candidatePaths('bash', ENV, 'win32')) {
89
89
  assert.ok(!candidate.toLowerCase().includes('msys2.exe'), candidate)
90
90
  }
91
91
  })
92
92
 
93
93
  test('cmd/wsl 候选:仅 System32 单点', () => {
94
- assert.deepEqual(candidatePaths('cmd', ENV), ['C:\\WINDOWS\\System32\\cmd.exe'])
95
- assert.deepEqual(candidatePaths('wsl', ENV), ['C:\\WINDOWS\\System32\\wsl.exe'])
94
+ assert.deepEqual(candidatePaths('cmd', ENV, 'win32'), ['C:\\WINDOWS\\System32\\cmd.exe'])
95
+ assert.deepEqual(candidatePaths('wsl', ENV, 'win32'), ['C:\\WINDOWS\\System32\\wsl.exe'])
96
96
  })
97
97
 
98
98
  test('显式路径原样使用,不探测', () => {
@@ -104,12 +104,12 @@ test('自动解析命中首个存在候选', () => {
104
104
  const resolved = resolveEntryPath({ kind: 'bash', path: '' }, stubExists([
105
105
  'C:\\one\\bash.exe',
106
106
  'C:\\PF\\Git\\bin\\bash.exe',
107
- ]), ENV)
107
+ ]), ENV, 'win32')
108
108
  assert.equal(resolved, 'C:\\PF\\Git\\bin\\bash.exe')
109
109
  })
110
110
 
111
111
  test('无候选存在返回 undefined', () => {
112
- assert.equal(resolveEntryPath({ kind: 'pwsh', path: '' }, stubExists([]), ENV), undefined)
112
+ assert.equal(resolveEntryPath({ kind: 'pwsh', path: '' }, stubExists([]), ENV, 'win32'), undefined)
113
113
  })
114
114
 
115
115
  test('detect 返回全部命中并去重', () => {
@@ -117,7 +117,7 @@ test('detect 返回全部命中并去重', () => {
117
117
  'C:\\PF\\Git\\bin\\bash.exe',
118
118
  'C:\\three\\bash.exe',
119
119
  'C:\\WINDOWS\\System32\\cmd.exe',
120
- ]))
120
+ ]), 'win32')
121
121
  assert.deepEqual(found, [
122
122
  { kind: 'bash', path: 'C:\\PF\\Git\\bin\\bash.exe' },
123
123
  { kind: 'bash', path: 'C:\\three\\bash.exe' },
@@ -126,6 +126,36 @@ test('detect 返回全部命中并去重', () => {
126
126
  })
127
127
 
128
128
  test('detect 同路径跨 kind 不重复收录', () => {
129
- const found = detectCandidates(['pwsh', 'bash'], ENV, stubExists(['C:\\one\\bash.exe', 'C:\\one\\pwsh.exe']))
129
+ const found = detectCandidates(['pwsh', 'bash'], ENV, stubExists(['C:\\one\\bash.exe', 'C:\\one\\pwsh.exe']), 'win32')
130
130
  assert.equal(found.length, 2)
131
131
  })
132
+
133
+ // POSIX 形态:冒号 PATH、裸可执行名、pwsh 固定锚点先于 PATH
134
+ const POSIX_ENV = { PATH: '/usr/local/bin:/opt/tools', SystemRoot: undefined }
135
+
136
+ test('POSIX pwsh 候选:固定锚点 → PATH 冒号各项(裸名)', () => {
137
+ const candidates = candidatePaths('pwsh', POSIX_ENV, 'linux')
138
+ assert.deepEqual(candidates, [
139
+ '/usr/bin/pwsh',
140
+ '/usr/local/bin/pwsh',
141
+ '/opt/microsoft/powershell/7/pwsh',
142
+ '/opt/homebrew/bin/pwsh',
143
+ '/usr/local/bin/pwsh',
144
+ '/opt/tools/pwsh',
145
+ ])
146
+ })
147
+
148
+ test('POSIX System32/cmd/wsl 锚位缺席', () => {
149
+ assert.deepEqual(candidatePaths('cmd', POSIX_ENV, 'linux'), [])
150
+ assert.deepEqual(candidatePaths('wsl', POSIX_ENV, 'linux'), [])
151
+ })
152
+
153
+ test('POSIX bash 候选:PATH 冒号各项(裸名,无 win32 锚位)', () => {
154
+ const candidates = candidatePaths('bash', POSIX_ENV, 'linux')
155
+ assert.deepEqual(candidates, ['/usr/local/bin/bash', '/opt/tools/bash'])
156
+ })
157
+
158
+ test('detect 平台参数化:同集合按平台取候选形态', () => {
159
+ const found = detectCandidates(['pwsh'], POSIX_ENV, stubExists(['/opt/tools/pwsh']), 'linux')
160
+ assert.deepEqual(found, [{ kind: 'pwsh', path: '/opt/tools/pwsh' }])
161
+ })
@@ -118,14 +118,27 @@ test('webServer 缺席:路由跳过不抛(带告警,headless 合法形态)', ()
118
118
  assert.match(registered.warnings.join('\n'), /webServer/)
119
119
  })
120
120
 
121
- test('webServer 在场:三条 exact 路由注册', () => {
121
+ test('webServer 在场:四条 exact 路由注册', () => {
122
122
  const { ctx, webServer } = stubCtx()
123
123
  webRoutes.apply(ctx)
124
124
  const paths = webServer.state.registered.map((item) => item.path).sort()
125
- assert.deepEqual(paths, ['/api/shell-select/config', '/api/shell-select/detect', '/api/shell-select/probe'])
125
+ assert.deepEqual(paths, ['/api/shell-select/config', '/api/shell-select/detect', '/api/shell-select/pathmap', '/api/shell-select/probe'])
126
126
  assert.ok(webServer.state.registered.every((item) => item.kind === 'exact'))
127
127
  })
128
128
 
129
+ test('GET /pathmap:返回宿主平台事实(platform/home/tmpDir)', async () => {
130
+ const { ctx } = stubCtx()
131
+ webRoutes.apply(ctx)
132
+ const handler = ctx.webServer.state.handlers.get('/api/shell-select/pathmap')
133
+ const res = fakeRes()
134
+ await handler(fakeReq('GET'), res)
135
+ assert.equal(res.state.status, 200)
136
+ const payload = JSON.parse(res.state.body)
137
+ assert.equal(payload.platform, process.platform)
138
+ assert.equal(typeof payload.home, 'string')
139
+ assert.equal(typeof payload.tmpDir, 'string')
140
+ })
141
+
129
142
  test('GET /config:返回 describe 值 + resolved 投影(路径已解析)', async () => {
130
143
  const { ctx } = stubCtx()
131
144
  webRoutes.apply(ctx)
@@ -1,27 +0,0 @@
1
- // KINDS 漂移守卫:client.js 工具形态清单与 host config.mjs 同源(竞品
2
- // bash-terminal-ts 的 drift 守卫同构:加形态忘改文案即测试失败)。
3
- // BDD 场景见 docs/feat-shell-select-optim/plan.md S14。
4
-
5
- import { test } from 'node:test'
6
- import assert from 'node:assert/strict'
7
- import { readFileSync } from 'node:fs'
8
- import { fileURLToPath } from 'node:url'
9
- import { dirname, join } from 'node:path'
10
- import { KINDS } from '../src/config.mjs'
11
-
12
- const here = dirname(fileURLToPath(import.meta.url))
13
- const clientSource = readFileSync(join(here, '..', 'src', 'client.js'), 'utf8')
14
-
15
- test('S14 client KINDS 字面与 host KINDS 同源', () => {
16
- const match = clientSource.match(/const KINDS = \[([^\]]*)\]/)
17
- assert.ok(match, 'client.js 缺少 KINDS 声明')
18
- const declared = match[1].split(',').map((item) => item.trim().replace(/^'|'$/g, '')).filter((item) => item.length > 0)
19
- assert.deepEqual(declared, [...KINDS])
20
- })
21
-
22
- test('S14b client KIND_LABELS 覆盖全部形态', () => {
23
- for (const kind of KINDS) {
24
- const pattern = new RegExp(kind + ":")
25
- assert.match(clientSource, pattern, 'KIND_LABELS 缺少形态 ' + kind)
26
- }
27
- })
@@ -1,37 +0,0 @@
1
- // 开关样式守卫 BDD:裸 checkbox 直接呈现违反仓库 client 规约(DEVELOPMENT/plugin-client-conventions.md),
2
- // 静态断言锁死 sls-switch 结构(参照 mce-switch/tn-switch/cb-switch 同款守卫)。
3
- import { readFileSync } from 'node:fs'
4
- import { dirname, join } from 'node:path'
5
- import { fileURLToPath } from 'node:url'
6
- import test from 'node:test'
7
- import assert from 'node:assert/strict'
8
-
9
- const PKG_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
10
- const source = readFileSync(join(PKG_ROOT, 'src', 'client.js'), 'utf8')
11
-
12
- test('switch 隐藏规则以 input[type="checkbox"] 精确匹配', () => {
13
- assert.match(source, /\.sls-switch input\[type="checkbox"\] \{ position:absolute/)
14
- })
15
-
16
- test('switch 状态选择器禁止裸 input 锚定(防误伤同 label 的文本输入)', () => {
17
- const bare = source.match(/\.sls-switch input:(?!\[type)[a-z-]+/g)
18
- assert.equal(bare, null, `裸 input 状态选择器: ${bare}`)
19
- })
20
-
21
- test('checkbox 仅允许出现在 switchToggle 工厂内,禁止裸 checkbox 直出', () => {
22
- const occurrences = [...source.matchAll(/type: 'checkbox'/g)].map((match) => match.index)
23
- assert.equal(occurrences.length, 1, `checkbox 字面量出现 ${occurrences.length} 次`)
24
- const factoryStart = source.indexOf('function switchToggle')
25
- const factoryEnd = source.indexOf('}', source.indexOf('__thumb', factoryStart))
26
- assert.ok(occurrences[0] > factoryStart && occurrences[0] < factoryEnd, 'checkbox 字面量不在 switchToggle 工厂内')
27
- })
28
-
29
- test('switchToggle 产出顺序为 input 在前 track 在后', () => {
30
- const factory = source.slice(source.indexOf('function switchToggle'))
31
- assert.ok(factory.indexOf("h('input'") < factory.indexOf('sls-switch__track'), '工厂内 input 必须先于 track')
32
- })
33
-
34
- test('checked/focus-visible 两态均以 checkbox 锚定', () => {
35
- assert.match(source, /\.sls-switch input\[type="checkbox"\]:checked \+/)
36
- assert.match(source, /\.sls-switch input\[type="checkbox"\]:focus-visible \+/)
37
- })