@xingular/harness 0.1.6 → 2.0.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/CHANGELOG.md +25 -0
- package/NOTICE.md +38 -0
- package/README.md +139 -28
- package/dist/cli.js +9598 -2201
- package/dist/terminal-pi.js +808 -16
- package/docs/README.md +15 -0
- package/docs/decisions/ADR-0001-owned-terminal.md +87 -0
- package/docs/decisions/ADR-0002-native-subagent-lifecycle.md +49 -0
- package/docs/decisions/ADR-0003-shared-asynchronous-subagents.md +68 -0
- package/docs/decisions/ADR-0004-fullscreen-terminal-layout.md +49 -0
- package/docs/decisions/ADR-0005-app-provided-oauth-clients.md +54 -0
- package/docs/terminal-tui.md +344 -0
- package/package.json +11 -4
- package/src/accounts/accounts.test.ts +197 -1
- package/src/accounts/accounts.ts +266 -28
- package/src/cli.ts +7 -3
- package/src/core/bus.test.ts +22 -0
- package/src/core/bus.ts +17 -2
- package/src/harness.ts +27 -4
- package/src/hub/catalog.json +123 -0
- package/src/hub/cli.ts +59 -0
- package/src/hub/hub.test.ts +205 -0
- package/src/hub/hub.ts +256 -0
- package/src/hub/schema.ts +81 -0
- package/src/index.ts +12 -2
- package/src/mcp/connections.ts +39 -12
- package/src/mcp/service.test.ts +49 -3
- package/src/mcp/service.ts +14 -3
- package/src/mcp/types.ts +10 -0
- package/src/memory/api.ts +16 -2
- package/src/memory/blocks.ts +3 -1
- package/src/memory/caller.ts +13 -1
- package/src/memory/curation.test.ts +206 -0
- package/src/memory/curation.ts +64 -0
- package/src/memory/dispatch.ts +5 -0
- package/src/memory/maintenance.test.ts +17 -1
- package/src/memory/maintenance.ts +367 -18
- package/src/memory/operations.ts +12 -0
- package/src/memory/proposals.ts +16 -8
- package/src/memory/recall.test.ts +11 -0
- package/src/memory/recall.ts +11 -3
- package/src/memory/trajectory.test.ts +178 -0
- package/src/memory/trajectory.ts +220 -0
- package/src/memory/wiki.ts +2 -1
- package/src/memory/wikiIndex.ts +3 -2
- package/src/plugins/builtin.ts +4 -2
- package/src/plugins/catalogue.test.ts +1 -1
- package/src/server/methods.ts +43 -2
- package/src/server/server.test.ts +20 -1
- package/src/sessions/approvals.test.ts +49 -0
- package/src/sessions/approvals.ts +26 -4
- package/src/sessions/contracts.ts +29 -3
- package/src/sessions/conversation.test.ts +83 -0
- package/src/sessions/lineageJoin.ts +1 -1
- package/src/sessions/policy.ts +4 -0
- package/src/sessions/questionBatches.test.ts +110 -0
- package/src/sessions/registry.ts +145 -27
- package/src/sessions/runStore.ts +14 -0
- package/src/sessions/runner.ts +45 -4
- package/src/sessions/sessions.test.ts +137 -4
- package/src/sessions/sessions.ts +51 -15
- package/src/sessions/subagentService.test.ts +159 -0
- package/src/sessions/subagentService.ts +236 -0
- package/src/sessions/subagents.ts +97 -0
- package/src/skills/registry.ts +16 -10
- package/src/terminal/accountsMenu.test.ts +225 -0
- package/src/terminal/accountsMenu.ts +382 -0
- package/src/terminal/control.test.ts +23 -0
- package/src/terminal/control.ts +23 -0
- package/src/terminal/controller.ts +134 -7
- package/src/terminal/hubMenu.ts +24 -0
- package/src/terminal/launch.test.ts +5 -14
- package/src/terminal/launch.ts +2 -42
- package/src/terminal/piExtension.lifecycle.test.ts +55 -0
- package/src/terminal/piExtension.ts +91 -6
- package/src/terminal/socket.test.ts +23 -0
- package/src/terminal/socket.ts +55 -20
- package/src/terminal/standalone.test.ts +37 -0
- package/src/terminal/standalone.ts +28 -7
- package/src/terminal/terminal.test.ts +220 -16
- package/src/terminal/terminals.ts +234 -16
- package/src/terminal/trajectory.test.ts +66 -0
- package/src/terminal/trajectory.ts +44 -0
- package/src/terminal/tui/application.test.ts +290 -0
- package/src/terminal/tui/application.ts +653 -0
- package/src/terminal/tui/commands.test.ts +278 -0
- package/src/terminal/tui/commands.ts +443 -0
- package/src/terminal/tui/dialogs.ts +301 -0
- package/src/terminal/tui/editor.test.ts +113 -0
- package/src/terminal/tui/editor.ts +109 -0
- package/src/terminal/tui/keybindings.test.ts +30 -0
- package/src/terminal/tui/keybindings.ts +59 -0
- package/src/terminal/tui/piAdapter.ts +62 -0
- package/src/terminal/tui/planMode.test.ts +10 -0
- package/src/terminal/tui/planMode.ts +12 -0
- package/src/terminal/tui/presentation.test.ts +79 -0
- package/src/terminal/tui/questions.test.ts +41 -0
- package/src/terminal/tui/questions.ts +47 -0
- package/src/terminal/tui/runtime.socket.test.ts +178 -0
- package/src/terminal/tui/runtime.test.ts +225 -0
- package/src/terminal/tui/runtime.ts +403 -0
- package/src/terminal/tui/screen.test.ts +143 -0
- package/src/terminal/tui/screen.ts +121 -0
- package/src/terminal/tui/sessionUi.ts +24 -0
- package/src/terminal/tui/settings.test.ts +150 -0
- package/src/terminal/tui/settings.ts +188 -0
- package/src/terminal/tui/skillInvocation.test.ts +71 -0
- package/src/terminal/tui/skillInvocation.ts +51 -0
- package/src/terminal/tui/status.test.ts +48 -0
- package/src/terminal/tui/status.ts +82 -0
- package/src/terminal/tui/theme.ts +185 -0
- package/src/terminal/tui/tools.test.ts +145 -0
- package/src/terminal/tui/tools.ts +142 -0
- package/src/terminal/tui/trajectoryPanel.test.ts +99 -0
- package/src/terminal/tui/trajectoryPanel.ts +226 -0
- package/src/terminal/tui/transcript.test.ts +39 -0
- package/src/terminal/tui/transcript.ts +135 -0
- package/src/terminal/tui/ui.test.ts +313 -0
- package/src/terminal/tui/ui.ts +166 -0
- package/src/terminal/tui/welcome.ts +102 -0
- package/src/terminal/tui/workingStatus.test.ts +44 -0
- package/src/terminal/tui/workingStatus.ts +238 -0
- package/src/tools/askUser.test.ts +69 -0
- package/src/tools/askUser.ts +31 -17
- package/src/tools/definitions.ts +42 -1
- package/src/tools/maintenance.ts +29 -0
- package/src/tools/memoryTools.ts +1 -1
- package/src/tools/registryWrite.test.ts +101 -0
- package/src/tools/registryWrite.ts +182 -0
- package/src/tools/spawnSubagent.ts +47 -0
- package/src/tools/types.ts +2 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { AgentSession, ExtensionUIContext } from '@earendil-works/pi-coding-agent'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { createTerminalCommands, type TerminalCommandHost } from './commands'
|
|
4
|
+
|
|
5
|
+
function setup() {
|
|
6
|
+
const ui = {
|
|
7
|
+
notify: vi.fn(), select: vi.fn(), confirm: vi.fn(), input: vi.fn(), custom: vi.fn(async () => undefined),
|
|
8
|
+
theme: { name: 'harness-titanium' }
|
|
9
|
+
} as unknown as ExtensionUIContext
|
|
10
|
+
const session = {
|
|
11
|
+
extensionRunner: { getRegisteredCommands: () => [{ name: 'accounts', description: 'Manage accounts' }] },
|
|
12
|
+
resourceLoader: {
|
|
13
|
+
getPrompts: () => ({ prompts: [{ name: 'review', description: 'Review code' }] }),
|
|
14
|
+
getSkills: () => ({ skills: [{ name: 'diagnose', description: 'Diagnose a problem' }] })
|
|
15
|
+
},
|
|
16
|
+
settingsManager: { getEnableSkillCommands: () => true },
|
|
17
|
+
compact: vi.fn(), reload: vi.fn(), setSessionName: vi.fn(),
|
|
18
|
+
getActiveToolNames: () => ['read', 'bash'],
|
|
19
|
+
getLastAssistantText: vi.fn(() => 'Actual assistant response'),
|
|
20
|
+
getSessionStats: () => ({ sessionId: 'real-session', tokens: { input: 7 } }),
|
|
21
|
+
exportToHtml: vi.fn(async (path: string) => path), exportToJsonl: vi.fn((path: string) => path)
|
|
22
|
+
} as unknown as AgentSession
|
|
23
|
+
const runtime = {
|
|
24
|
+
newSession: vi.fn(), switchSession: vi.fn(), fork: vi.fn(), importFromJsonl: vi.fn(), reload: vi.fn(), navigateTree: vi.fn(),
|
|
25
|
+
installationDeviceId: vi.fn(async () => '12345678-1234-4234-8234-123456789abc'),
|
|
26
|
+
permissions: vi.fn(async () => ({ full: false, globalTools: [] as string[] })),
|
|
27
|
+
setFullPermission: vi.fn(async () => {}), setGlobalTool: vi.fn(async () => {})
|
|
28
|
+
}
|
|
29
|
+
const trajectoryRun = { id: 'run-1', mode: 'code', goal: 'Review the change', startedAt: '2026-10-18T00:00:00.000Z', endedAt: '2026-10-18T00:00:01.000Z', status: 'completed' }
|
|
30
|
+
const host: TerminalCommandHost = {
|
|
31
|
+
session: () => session, runtime: () => runtime as unknown as ReturnType<TerminalCommandHost['runtime']>, ui,
|
|
32
|
+
cwd: () => '/isolated/project', sessionDirectory: () => '/isolated/account/history',
|
|
33
|
+
refresh: vi.fn(), changed: vi.fn(), shutdown: vi.fn(), setEditorText: vi.fn(), copyText: vi.fn(async () => {}),
|
|
34
|
+
hotkeys: () => 'Escape: interrupt', changelog: async () => 'Harness release notes',
|
|
35
|
+
trajectory: {
|
|
36
|
+
list: vi.fn(async () => [trajectoryRun]),
|
|
37
|
+
read: vi.fn(async () => ({ run: trajectoryRun, events: [] }))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return { host, session, runtime, ui, commands: createTerminalCommands(host) }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
describe('owned terminal commands', () => {
|
|
44
|
+
it('leaves ordinary prompts and supported extension/resource commands for the SDK', async () => {
|
|
45
|
+
const { commands, ui } = setup()
|
|
46
|
+
for (const text of ['hello', '/accounts', '/review module', '/skill:diagnose trace']) {
|
|
47
|
+
expect(await commands.execute(text)).toBe(false)
|
|
48
|
+
}
|
|
49
|
+
expect(ui.notify).not.toHaveBeenCalled()
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
it('consumes unknown slash commands without sending them to the model', async () => {
|
|
53
|
+
const { commands, ui } = setup()
|
|
54
|
+
expect(await commands.execute('/typo dangerous operation')).toBe(true)
|
|
55
|
+
expect(ui.notify).toHaveBeenCalledWith(expect.stringContaining('Unknown command: /typo'), 'error')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('populates completion from builtins and loaded resources', () => {
|
|
59
|
+
const { commands } = setup()
|
|
60
|
+
const names = commands.completions().map(item => item.name)
|
|
61
|
+
expect(names).toEqual(expect.arrayContaining(['model', 'resume', 'accounts', 'review', 'effort', 'trajectory', 'permissions']))
|
|
62
|
+
expect(names).not.toContain('thinking')
|
|
63
|
+
expect(names).not.toContain('skill:diagnose')
|
|
64
|
+
expect(new Set(names).size).toBe(names.length)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('invokes a loaded skill as $name, and leaves other text for the SDK', () => {
|
|
68
|
+
const { commands, session } = setup()
|
|
69
|
+
expect(commands.skills()).toEqual([{ name: 'diagnose', description: 'Diagnose a problem' }])
|
|
70
|
+
expect(commands.expandSkill('$diagnose the trace')).toBe('/skill:diagnose the trace')
|
|
71
|
+
expect(commands.expandSkill('$unknown the trace')).toBe('$unknown the trace')
|
|
72
|
+
expect(commands.expandSkill('/skill:diagnose trace')).toBe('/skill:diagnose trace')
|
|
73
|
+
vi.spyOn(session.settingsManager, 'getEnableSkillCommands').mockReturnValue(false)
|
|
74
|
+
expect(commands.skills()).toEqual([])
|
|
75
|
+
expect(commands.expandSkill('$diagnose the trace')).toBe('$diagnose the trace')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
it('uses the scope checked runtime for explicit resume and import paths', async () => {
|
|
79
|
+
const { commands, runtime } = setup()
|
|
80
|
+
await commands.execute('/resume /isolated/account/history/session.jsonl')
|
|
81
|
+
expect(runtime.switchSession).toHaveBeenCalledWith('/isolated/account/history/session.jsonl')
|
|
82
|
+
await commands.execute('/import other.jsonl')
|
|
83
|
+
expect(runtime.importFromJsonl).toHaveBeenCalledWith('/isolated/project/other.jsonl')
|
|
84
|
+
await commands.execute('/new')
|
|
85
|
+
expect(runtime.newSession).toHaveBeenCalledOnce()
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('passes the Harness installation device ID to direct OAuth sign-in', async () => {
|
|
89
|
+
const { commands, session, runtime } = setup()
|
|
90
|
+
const login = vi.fn(async (_provider: string, _type: string, _interaction: unknown, options?: { getDeviceId?: () => string }) => options)
|
|
91
|
+
Object.assign(session, { modelRuntime: {
|
|
92
|
+
getProviders: () => [{ id: 'openai', name: 'OpenAI', auth: { oauth: {}, apiKey: undefined } }],
|
|
93
|
+
login
|
|
94
|
+
} })
|
|
95
|
+
|
|
96
|
+
await commands.execute('/login openai')
|
|
97
|
+
|
|
98
|
+
expect(runtime.installationDeviceId).toHaveBeenCalledOnce()
|
|
99
|
+
expect(login.mock.calls[0]?.[3]?.getDeviceId?.()).toBe('12345678-1234-4234-8234-123456789abc')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('reports rejected navigation without claiming success', async () => {
|
|
103
|
+
const { commands, runtime, ui } = setup()
|
|
104
|
+
runtime.switchSession.mockRejectedValue(new Error('Session is outside the prepared project scope'))
|
|
105
|
+
expect(await commands.execute('/resume /isolated/account/history/session.jsonl')).toBe(true)
|
|
106
|
+
expect(ui.notify).toHaveBeenCalledWith('Session is outside the prepared project scope', 'error')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('copies actual assistant text and handles missing responses', async () => {
|
|
110
|
+
const { commands, host, session, ui } = setup()
|
|
111
|
+
await commands.execute('/copy')
|
|
112
|
+
expect(host.copyText).toHaveBeenCalledWith('Actual assistant response')
|
|
113
|
+
vi.mocked(session.getLastAssistantText).mockReturnValue(undefined)
|
|
114
|
+
await commands.execute('/copy')
|
|
115
|
+
expect(ui.notify).toHaveBeenCalledWith('No assistant response to copy yet', 'error')
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
it('keeps multiline compaction instructions and session names', async () => {
|
|
119
|
+
const { commands, session } = setup()
|
|
120
|
+
await commands.execute('/compact Keep the API\nand authentication details')
|
|
121
|
+
expect(session.compact).toHaveBeenCalledWith('Keep the API\nand authentication details')
|
|
122
|
+
await commands.execute('/name Unicode 知識 🧠')
|
|
123
|
+
expect(session.setSessionName).toHaveBeenCalledWith('Unicode 知識 🧠')
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('exports supported formats through native session APIs', async () => {
|
|
127
|
+
const { commands, session } = setup()
|
|
128
|
+
await commands.execute('/export notes.jsonl')
|
|
129
|
+
expect(session.exportToJsonl).toHaveBeenCalledWith('/isolated/project/notes.jsonl')
|
|
130
|
+
await commands.execute('/export notes.html')
|
|
131
|
+
expect(session.exportToHtml).toHaveBeenCalledWith('/isolated/project/notes.html', { themeName: 'harness-titanium' })
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('does not upload anything after a declined share confirmation', async () => {
|
|
135
|
+
const { commands, ui, session } = setup()
|
|
136
|
+
vi.mocked(ui.confirm).mockResolvedValue(false)
|
|
137
|
+
await commands.execute('/share')
|
|
138
|
+
expect(ui.confirm).toHaveBeenCalledOnce()
|
|
139
|
+
expect(session.exportToHtml).not.toHaveBeenCalled()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('shows actual application keyboard bindings and release notes', async () => {
|
|
143
|
+
const { commands, ui } = setup()
|
|
144
|
+
await commands.execute('/hotkeys')
|
|
145
|
+
expect(ui.notify).toHaveBeenCalledWith('Escape: interrupt', 'info')
|
|
146
|
+
await commands.execute('/changelog')
|
|
147
|
+
expect(ui.notify).toHaveBeenCalledWith('Harness release notes', 'info')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('opens the latest or requested trajectory run in the interactive overlay', async () => {
|
|
151
|
+
const { commands, host, ui } = setup()
|
|
152
|
+
await commands.execute('/trajectory')
|
|
153
|
+
expect(host.trajectory.list).toHaveBeenCalledOnce()
|
|
154
|
+
expect(host.trajectory.read).toHaveBeenCalledWith('run-1')
|
|
155
|
+
expect(ui.custom).toHaveBeenCalledOnce()
|
|
156
|
+
expect(ui.custom).toHaveBeenCalledWith(expect.any(Function), { overlay: true, overlayOptions: { width: '100%', maxHeight: '90%' } })
|
|
157
|
+
|
|
158
|
+
await commands.execute('/trajectory run-1')
|
|
159
|
+
expect(host.trajectory.read).toHaveBeenLastCalledWith('run-1')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('shows shared tool grants and confirms Full Permission before enabling it', async () => {
|
|
163
|
+
const { commands, host, runtime, ui } = setup()
|
|
164
|
+
vi.mocked(ui.custom).mockImplementationOnce(async () => 'tool:bash' as never)
|
|
165
|
+
.mockImplementationOnce(async () => 'done' as never)
|
|
166
|
+
await commands.execute('/permissions')
|
|
167
|
+
expect(runtime.permissions).toHaveBeenCalled()
|
|
168
|
+
expect(runtime.setGlobalTool).toHaveBeenCalledWith('bash', true)
|
|
169
|
+
|
|
170
|
+
vi.mocked(ui.custom).mockImplementationOnce(async () => 'full' as never)
|
|
171
|
+
.mockImplementationOnce(async () => 'done' as never)
|
|
172
|
+
vi.mocked(ui.confirm).mockResolvedValueOnce(true)
|
|
173
|
+
await commands.execute('/permissions')
|
|
174
|
+
expect(ui.confirm).toHaveBeenCalledWith('Enable Full Permission?', expect.stringContaining('Hard denials'))
|
|
175
|
+
expect(runtime.setFullPermission).toHaveBeenCalledWith(true)
|
|
176
|
+
expect(host.refresh).toHaveBeenCalled()
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
it('reports empty history and refuses run IDs outside the current native session', async () => {
|
|
180
|
+
const { commands, host, ui } = setup()
|
|
181
|
+
vi.mocked(host.trajectory.list).mockResolvedValue([])
|
|
182
|
+
await commands.execute('/trajectory')
|
|
183
|
+
expect(ui.notify).toHaveBeenCalledWith('No runs recorded for this native session yet', 'info')
|
|
184
|
+
expect(ui.custom).not.toHaveBeenCalled()
|
|
185
|
+
|
|
186
|
+
vi.mocked(host.trajectory.list).mockResolvedValue([{ id: 'run-1', mode: 'code', goal: null, startedAt: '2026-10-18T00:00:00.000Z', endedAt: null, status: 'running' }])
|
|
187
|
+
await commands.execute('/trajectory other-run')
|
|
188
|
+
expect(host.trajectory.read).not.toHaveBeenCalled()
|
|
189
|
+
expect(ui.notify).toHaveBeenLastCalledWith('Run not found in this native session', 'error')
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it('exits through the application lifecycle', async () => {
|
|
193
|
+
const { commands, host } = setup()
|
|
194
|
+
await commands.execute('/quit')
|
|
195
|
+
expect(host.shutdown).toHaveBeenCalledOnce()
|
|
196
|
+
})
|
|
197
|
+
it('validates thinking levels before mutating the session', async () => {
|
|
198
|
+
const { commands, session, ui } = setup()
|
|
199
|
+
Object.assign(session, { getAvailableThinkingLevels: () => ['off', 'high'], setThinkingLevel: vi.fn() })
|
|
200
|
+
await commands.execute('/effort high')
|
|
201
|
+
expect(session.setThinkingLevel).toHaveBeenCalledWith('high', { persist: true })
|
|
202
|
+
await commands.execute('/effort fabricated')
|
|
203
|
+
expect(session.setThinkingLevel).toHaveBeenCalledTimes(1)
|
|
204
|
+
expect(ui.notify).toHaveBeenCalledWith('Available thinking levels: off, high', 'error')
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
it('clones the real leaf through the runtime without inventing history', async () => {
|
|
208
|
+
const { commands, session, runtime, host } = setup()
|
|
209
|
+
Object.assign(session, { sessionManager: { getLeafId: () => 'leaf-id' } })
|
|
210
|
+
runtime.fork.mockResolvedValue({ cancelled: false })
|
|
211
|
+
await commands.execute('/clone')
|
|
212
|
+
expect(runtime.fork).toHaveBeenCalledWith('leaf-id', { position: 'at' })
|
|
213
|
+
expect(host.setEditorText).toHaveBeenCalledWith('')
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('uses masked input for secret provider credentials', async () => {
|
|
217
|
+
const { commands, session, host, ui } = setup()
|
|
218
|
+
const login = vi.fn(async (_provider, _type, interaction) => {
|
|
219
|
+
expect(await interaction.prompt({ type: 'secret', message: 'API key' })).toBe('private-key')
|
|
220
|
+
})
|
|
221
|
+
Object.assign(session, { modelRuntime: {
|
|
222
|
+
getProviders: () => [{ id: 'provider', name: 'Provider', auth: { apiKey: { login: true } } }], login
|
|
223
|
+
} })
|
|
224
|
+
host.secretInput = vi.fn(async () => 'private-key')
|
|
225
|
+
await commands.execute('/login provider')
|
|
226
|
+
expect(host.secretInput).toHaveBeenCalledWith('API key', undefined, { signal: undefined })
|
|
227
|
+
expect(ui.input).not.toHaveBeenCalled()
|
|
228
|
+
expect(ui.notify).not.toHaveBeenCalledWith(expect.stringContaining('private-key'), expect.anything())
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('fails closed when secure credential input is unavailable', async () => {
|
|
232
|
+
const { commands, session, ui } = setup()
|
|
233
|
+
Object.assign(session, { modelRuntime: {
|
|
234
|
+
getProviders: () => [{ id: 'provider', name: 'Provider', auth: { apiKey: { login: true } } }],
|
|
235
|
+
login: async (_provider: string, _type: string, interaction: { prompt(prompt: unknown): Promise<string> }) => interaction.prompt({ type: 'secret', message: 'API key' })
|
|
236
|
+
} })
|
|
237
|
+
await commands.execute('/login provider')
|
|
238
|
+
expect(ui.input).not.toHaveBeenCalled()
|
|
239
|
+
expect(ui.notify).toHaveBeenCalledWith('Secure credential entry is unavailable', 'error')
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('rejects resume paths outside account history before runtime navigation', async () => {
|
|
243
|
+
const { commands, runtime, ui } = setup()
|
|
244
|
+
await commands.execute('/resume /other-account/session.jsonl')
|
|
245
|
+
expect(runtime.switchSession).not.toHaveBeenCalled()
|
|
246
|
+
expect(ui.notify).toHaveBeenCalledWith(expect.stringContaining('this account history folder'), 'error')
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
it('routes trust through the existing protected plugin workflow', async () => {
|
|
250
|
+
const { commands, session } = setup()
|
|
251
|
+
Object.assign(session, { prompt: vi.fn(async () => {}) })
|
|
252
|
+
vi.spyOn(session.extensionRunner, 'getRegisteredCommands').mockReturnValue([{ name: 'plugins' }] as never)
|
|
253
|
+
await commands.execute('/trust')
|
|
254
|
+
expect(session.prompt).toHaveBeenCalledWith('/plugins')
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
it('opens a reviewable Harness issue draft without attaching session history', async () => {
|
|
258
|
+
const { commands, host, ui } = setup()
|
|
259
|
+
host.openExternal = vi.fn(async () => {})
|
|
260
|
+
vi.mocked(ui.confirm).mockResolvedValue(false)
|
|
261
|
+
await commands.execute('/bug Unicode issue 知識')
|
|
262
|
+
expect(host.openExternal).not.toHaveBeenCalled()
|
|
263
|
+
vi.mocked(ui.confirm).mockResolvedValue(true)
|
|
264
|
+
await commands.execute('/bug Unicode issue 知識')
|
|
265
|
+
const url = new URL(vi.mocked(host.openExternal).mock.calls[0][0])
|
|
266
|
+
expect(url.pathname).toBe('/tanntnny/xingular-harness/issues/new')
|
|
267
|
+
expect(url.searchParams.get('title')).toBe('Unicode issue 知識')
|
|
268
|
+
expect(url.searchParams.get('body')).toContain('Harness')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('reloads through the stable runtime lifecycle', async () => {
|
|
272
|
+
const { commands, runtime, session } = setup()
|
|
273
|
+
await commands.execute('/reload')
|
|
274
|
+
expect(runtime.reload).toHaveBeenCalledOnce()
|
|
275
|
+
expect(session.reload).not.toHaveBeenCalled()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
})
|