@raidiant/notifai 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -4
- package/dist/atomic-file.d.ts +4 -0
- package/dist/atomic-file.js +60 -10
- package/dist/atomic-file.js.map +1 -1
- package/dist/client.d.ts +25 -2
- package/dist/client.js +45 -1
- package/dist/client.js.map +1 -1
- package/dist/commands.d.ts +56 -7
- package/dist/commands.js +980 -318
- package/dist/commands.js.map +1 -1
- package/dist/config-schema.d.ts +1 -1
- package/dist/config-schema.js +46 -8
- package/dist/config-schema.js.map +1 -1
- package/dist/config.d.ts +38 -3
- package/dist/config.js +61 -14
- package/dist/config.js.map +1 -1
- package/dist/file-lock.d.ts +26 -0
- package/dist/file-lock.js +268 -0
- package/dist/file-lock.js.map +1 -0
- package/dist/harnesses.d.ts +28 -0
- package/dist/harnesses.js +40 -0
- package/dist/harnesses.js.map +1 -0
- package/dist/hook-adapter.d.ts +27 -0
- package/dist/hook-adapter.js +188 -0
- package/dist/hook-adapter.js.map +1 -0
- package/dist/hook-input.d.ts +10 -0
- package/dist/hook-input.js +64 -0
- package/dist/hook-input.js.map +1 -0
- package/dist/hooks.d.ts +66 -10
- package/dist/hooks.js +751 -276
- package/dist/hooks.js.map +1 -1
- package/dist/install-hooks.d.ts +40 -16
- package/dist/install-hooks.js +217 -73
- package/dist/install-hooks.js.map +1 -1
- package/dist/interactive.d.ts +1 -1
- package/dist/interactive.js +3 -3
- package/dist/interactive.js.map +1 -1
- package/dist/logging.d.ts +142 -0
- package/dist/logging.js +533 -0
- package/dist/logging.js.map +1 -0
- package/dist/main.js +97 -36
- package/dist/main.js.map +1 -1
- package/dist/opencode-plugin.d.ts +7 -7
- package/dist/opencode-plugin.js +12 -13
- package/dist/opencode-plugin.js.map +1 -1
- package/dist/ui/config-view.js +3 -0
- package/dist/ui/config-view.js.map +1 -1
- package/dist/ui/theme.d.ts +1 -1
- package/package.json +3 -2
- package/src/atomic-file.ts +86 -7
- package/src/claude-wake.test.ts +320 -0
- package/src/claude-wake.ts +489 -0
- package/src/client.test.ts +12 -0
- package/src/client.ts +72 -4
- package/src/codex-wake.test.ts +253 -0
- package/src/codex-wake.ts +288 -0
- package/src/commands.test.ts +1261 -77
- package/src/commands.ts +1482 -375
- package/src/config-schema.test.ts +3 -3
- package/src/config-schema.ts +57 -44
- package/src/config.test.ts +22 -16
- package/src/config.ts +81 -66
- package/src/file-lock-process-worker.test.ts +64 -0
- package/src/file-lock.test.ts +279 -0
- package/src/file-lock.ts +318 -0
- package/src/harnesses.test.ts +34 -0
- package/src/harnesses.ts +57 -0
- package/src/hook-adapter.test.ts +187 -0
- package/src/hook-adapter.ts +212 -0
- package/src/hook-input.test.ts +39 -0
- package/src/hook-input.ts +72 -0
- package/src/hooks.test.ts +1985 -417
- package/src/hooks.ts +1338 -461
- package/src/install-hooks.test.ts +131 -41
- package/src/install-hooks.ts +284 -84
- package/src/interactive.ts +3 -8
- package/src/logging-process-worker.test.ts +39 -0
- package/src/logging.test.ts +453 -0
- package/src/logging.ts +639 -0
- package/src/main.ts +113 -32
- package/src/opencode-plugin.ts +15 -16
- package/src/session-end-log-lock-worker.test.ts +35 -0
- package/src/skill.test.ts +34 -0
- package/src/ui/config-view.test.ts +9 -7
- package/src/ui/config-view.ts +3 -0
- package/src/idle.test.ts +0 -32
- package/src/idle.ts +0 -41
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
codexLayerDir,
|
|
28
28
|
codexProjectRoot,
|
|
29
29
|
findInstallations,
|
|
30
|
+
codexHookIdentityHash,
|
|
31
|
+
codexTrustKey,
|
|
32
|
+
codexTrustProblems,
|
|
30
33
|
handlerEvent,
|
|
31
34
|
hookCommand,
|
|
32
35
|
mergeHooks,
|
|
@@ -38,9 +41,10 @@ import {
|
|
|
38
41
|
|
|
39
42
|
const SCRIPT = '/opt/notifai/dist/main.js'
|
|
40
43
|
const EXEC = '/usr/local/bin/node'
|
|
44
|
+
const ADAPTER = '/Users/test/.notifai/bin/hook-adapter'
|
|
41
45
|
|
|
42
46
|
function ours(): HookConfig {
|
|
43
|
-
return buildHookConfig({
|
|
47
|
+
return buildHookConfig({ adapterPath: ADAPTER })
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
describe('choice labels', () => {
|
|
@@ -71,21 +75,22 @@ describe('choice labels', () => {
|
|
|
71
75
|
})
|
|
72
76
|
|
|
73
77
|
describe('hook config', () => {
|
|
74
|
-
it('invokes
|
|
75
|
-
expect(hookCommand(
|
|
78
|
+
it('invokes one stable adapter so a sparse hook PATH still works', () => {
|
|
79
|
+
expect(hookCommand(ADAPTER, 'stop')).toBe(`'${ADAPTER}' hook stop --owner notifai`)
|
|
76
80
|
})
|
|
77
81
|
|
|
78
82
|
it('single-quotes so a path cannot become a command', () => {
|
|
79
83
|
// Double quotes still expand $() and backticks, so a checkout path with
|
|
80
84
|
// shell syntax was execution on every hook event.
|
|
81
|
-
const cmd = hookCommand('/
|
|
82
|
-
expect(cmd).toBe("'/
|
|
83
|
-
expect(hookCommand(
|
|
85
|
+
const cmd = hookCommand('/tmp/$(touch /tmp/pwned)/hook-adapter', 'stop')
|
|
86
|
+
expect(cmd).toBe("'/tmp/$(touch /tmp/pwned)/hook-adapter' hook stop --owner notifai")
|
|
87
|
+
expect(hookCommand("/it's/here", 'stop')).toContain(`'/it'\\''s/here'`)
|
|
84
88
|
})
|
|
85
89
|
|
|
86
|
-
it('
|
|
87
|
-
const config = buildHookConfig({
|
|
88
|
-
expect(config['Stop']?.[0]?.hooks[0]?.timeout).toBe(
|
|
90
|
+
it('uses one fixed blocking process budget rather than accepting mutable timing inputs', () => {
|
|
91
|
+
const config = buildHookConfig({ adapterPath: ADAPTER })
|
|
92
|
+
expect(config['Stop']?.[0]?.hooks[0]?.timeout).toBe(540)
|
|
93
|
+
expect(config['Stop']?.[0]?.hooks[0]?.async).toBeUndefined()
|
|
89
94
|
// Claude Code caps UserPromptSubmit at 30s; Codex gives SessionEnd 1-3s.
|
|
90
95
|
expect(config['UserPromptSubmit']?.[0]?.hooks[0]?.timeout).toBe(15)
|
|
91
96
|
expect(config['SessionEnd']?.[0]?.hooks[0]?.timeout).toBe(3)
|
|
@@ -93,10 +98,7 @@ describe('hook config', () => {
|
|
|
93
98
|
|
|
94
99
|
it('stamps the exact harness on every generated command', () => {
|
|
95
100
|
const config = buildHookConfig({
|
|
96
|
-
|
|
97
|
-
scriptPath: SCRIPT,
|
|
98
|
-
replyTimeoutSeconds: 240,
|
|
99
|
-
graceSeconds: 0,
|
|
101
|
+
adapterPath: ADAPTER,
|
|
100
102
|
harness: 'claude-code',
|
|
101
103
|
})
|
|
102
104
|
expect(
|
|
@@ -106,6 +108,56 @@ describe('hook config', () => {
|
|
|
106
108
|
.every((handler) => handler.command.includes('--harness claude-code')),
|
|
107
109
|
).toBe(true)
|
|
108
110
|
})
|
|
111
|
+
|
|
112
|
+
it('lets Codex own Stop timeout while keeping short fixed lifecycle budgets', () => {
|
|
113
|
+
const codex = buildHookConfig({ adapterPath: ADAPTER, harness: 'codex' })
|
|
114
|
+
|
|
115
|
+
expect(codex['UserPromptSubmit']?.[0]?.hooks[0]?.timeout).toBe(15)
|
|
116
|
+
expect(codex['SessionEnd']?.[0]?.hooks[0]?.timeout).toBe(3)
|
|
117
|
+
// Codex hashes the exact serialized definition into `trusted_hash`, so its
|
|
118
|
+
// Stop handler stays a bare blocking command: declaring a timeout would
|
|
119
|
+
// invalidate the user's approval and the handler would silently stop.
|
|
120
|
+
expect(codex['Stop']?.[0]?.hooks[0]).toEqual({
|
|
121
|
+
type: 'command',
|
|
122
|
+
command: hookCommand(ADAPTER, 'stop', 'codex'),
|
|
123
|
+
})
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('gives Claude Code an asynchronous Stop with an explicit waiter budget', () => {
|
|
127
|
+
const claude = buildHookConfig({ adapterPath: ADAPTER, harness: 'claude-code' })
|
|
128
|
+
|
|
129
|
+
// `async: true` is what frees the turn: the handler returns at once and the
|
|
130
|
+
// same process lives on as the waiter. The explicit timeout is what keeps
|
|
131
|
+
// that process alive — Claude kills a background hook at its own 600s
|
|
132
|
+
// default and reports nothing, so a wait near that boundary loses answers.
|
|
133
|
+
expect(claude['Stop']?.[0]?.hooks[0]).toEqual({
|
|
134
|
+
type: 'command',
|
|
135
|
+
command: hookCommand(ADAPTER, 'stop', 'claude-code'),
|
|
136
|
+
timeout: 3600,
|
|
137
|
+
async: true,
|
|
138
|
+
})
|
|
139
|
+
expect(claude['UserPromptSubmit']?.[0]?.hooks[0]?.async).toBeUndefined()
|
|
140
|
+
expect(claude['SessionEnd']?.[0]?.hooks[0]?.async).toBeUndefined()
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('leaves exactly one handler per event when an old blocking shape is reinstalled over', () => {
|
|
144
|
+
const old = mergeHooks({}, buildHookConfig({ adapterPath: ADAPTER }), SCRIPT)
|
|
145
|
+
const migrated = mergeHooks(
|
|
146
|
+
old.document,
|
|
147
|
+
buildHookConfig({ adapterPath: ADAPTER, harness: 'claude-code' }),
|
|
148
|
+
SCRIPT,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
for (const event of ['UserPromptSubmit', 'Stop', 'SessionEnd']) {
|
|
152
|
+
const groups = migrated.document.hooks?.[event] ?? []
|
|
153
|
+
expect(groups.flatMap((group) => group.hooks)).toHaveLength(1)
|
|
154
|
+
}
|
|
155
|
+
expect(migrated.document.hooks?.['Stop']?.[0]?.hooks[0]).toMatchObject({
|
|
156
|
+
timeout: 3600,
|
|
157
|
+
async: true,
|
|
158
|
+
})
|
|
159
|
+
expect(migrated.replaced.sort()).toEqual(['SessionEnd', 'Stop', 'UserPromptSubmit'])
|
|
160
|
+
})
|
|
109
161
|
})
|
|
110
162
|
|
|
111
163
|
describe('merging into existing settings', () => {
|
|
@@ -130,11 +182,11 @@ describe('merging into existing settings', () => {
|
|
|
130
182
|
const once = mergeHooks({}, ours(), SCRIPT)
|
|
131
183
|
const twice = mergeHooks(
|
|
132
184
|
once.document,
|
|
133
|
-
buildHookConfig({
|
|
185
|
+
buildHookConfig({ adapterPath: ADAPTER }),
|
|
134
186
|
SCRIPT,
|
|
135
187
|
)
|
|
136
188
|
expect(twice.document.hooks?.['Stop']).toHaveLength(1)
|
|
137
|
-
expect(twice.document.hooks?.['Stop']?.[0]?.hooks[0]?.timeout).toBe(
|
|
189
|
+
expect(twice.document.hooks?.['Stop']?.[0]?.hooks[0]?.timeout).toBe(540)
|
|
138
190
|
expect(twice.replaced).toContain('Stop')
|
|
139
191
|
})
|
|
140
192
|
|
|
@@ -292,9 +344,9 @@ describe('upgrading past a dropped event', () => {
|
|
|
292
344
|
const stale = {
|
|
293
345
|
hooks: {
|
|
294
346
|
PermissionRequest: [
|
|
295
|
-
{ hooks: [{ type: 'command' as const, command: hookCommand(
|
|
347
|
+
{ hooks: [{ type: 'command' as const, command: hookCommand(ADAPTER, 'permission-request') }] },
|
|
296
348
|
],
|
|
297
|
-
Stop: [{ hooks: [{ type: 'command' as const, command: hookCommand(
|
|
349
|
+
Stop: [{ hooks: [{ type: 'command' as const, command: hookCommand(ADAPTER, 'stop') }] }],
|
|
298
350
|
},
|
|
299
351
|
}
|
|
300
352
|
|
|
@@ -311,7 +363,7 @@ describe('upgrading past a dropped event', () => {
|
|
|
311
363
|
PermissionRequest: [
|
|
312
364
|
{
|
|
313
365
|
hooks: [
|
|
314
|
-
{ type: 'command' as const, command: hookCommand(
|
|
366
|
+
{ type: 'command' as const, command: hookCommand(ADAPTER, 'permission-request') },
|
|
315
367
|
{ type: 'command' as const, command: 'their-own-tool --check' },
|
|
316
368
|
],
|
|
317
369
|
},
|
|
@@ -393,10 +445,49 @@ describe('finding what is installed', () => {
|
|
|
393
445
|
it('reads the event a handler actually invokes, not the key it sits under', () => {
|
|
394
446
|
// These diverge exactly when an upgrade drops an event, which is the case
|
|
395
447
|
// worth detecting.
|
|
396
|
-
expect(handlerEvent(hookCommand(
|
|
448
|
+
expect(handlerEvent(hookCommand(ADAPTER, 'permission-request'))).toBe('permission-request')
|
|
397
449
|
expect(handlerEvent('unrelated --tool')).toBeNull()
|
|
398
450
|
})
|
|
399
451
|
|
|
452
|
+
it('detects when Codex still trusts an older definition of an installed hook', () => {
|
|
453
|
+
const cwd = mkdtempSync(path.join(os.tmpdir(), 'notifai-codex-trust-'))
|
|
454
|
+
const codexHome = path.join(cwd, 'codex-home')
|
|
455
|
+
const hookFile = path.join(cwd, '.codex', 'hooks.json')
|
|
456
|
+
mkdirSync(path.dirname(hookFile), { recursive: true })
|
|
457
|
+
applyPlan(hookFile, { hooks: ours() })
|
|
458
|
+
const installations = findInstallations(cwd, { CODEX_HOME: codexHome })
|
|
459
|
+
const codex = installations.find((installation) => installation.harness === 'codex')
|
|
460
|
+
const stop = codex?.handlers.find((handler) => handler.event === 'Stop')
|
|
461
|
+
expect(stop).toBeDefined()
|
|
462
|
+
|
|
463
|
+
mkdirSync(codexHome, { recursive: true })
|
|
464
|
+
writeFileSync(
|
|
465
|
+
path.join(codexHome, 'config.toml'),
|
|
466
|
+
`[hooks.state.${JSON.stringify(codexTrustKey(codex!, stop!))}]\ntrusted_hash = "sha256:obsolete"\n`,
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
const stale = codexTrustProblems(installations, { CODEX_HOME: codexHome })
|
|
470
|
+
expect(stale).toHaveLength(3)
|
|
471
|
+
expect(stale).toEqual(expect.arrayContaining([expect.stringMatching(/Stop.*changed.*\/hooks/i)]))
|
|
472
|
+
|
|
473
|
+
writeFileSync(
|
|
474
|
+
path.join(codexHome, 'config.toml'),
|
|
475
|
+
`[hooks.state.${JSON.stringify(codexTrustKey(codex!, stop!))}]\ntrusted_hash = "${codexHookIdentityHash(stop!)}"\n`,
|
|
476
|
+
)
|
|
477
|
+
expect(codexTrustProblems(installations, { CODEX_HOME: codexHome })).toEqual([
|
|
478
|
+
expect.stringMatching(/UserPromptSubmit.*not trusted.*\/hooks/i),
|
|
479
|
+
expect.stringMatching(/SessionEnd.*not trusted.*\/hooks/i),
|
|
480
|
+
])
|
|
481
|
+
|
|
482
|
+
writeFileSync(
|
|
483
|
+
path.join(codexHome, 'config.toml'),
|
|
484
|
+
`[hooks.state.${JSON.stringify(codexTrustKey(codex!, stop!))}]\ntrusted_hash = "${codexHookIdentityHash(stop!)}"\nenabled = false\n`,
|
|
485
|
+
)
|
|
486
|
+
expect(codexTrustProblems(installations, { CODEX_HOME: codexHome })).toEqual(
|
|
487
|
+
expect.arrayContaining([expect.stringMatching(/Stop.*disabled.*\/hooks/i)]),
|
|
488
|
+
)
|
|
489
|
+
})
|
|
490
|
+
|
|
400
491
|
})
|
|
401
492
|
|
|
402
493
|
/**
|
|
@@ -405,7 +496,7 @@ describe('finding what is installed', () => {
|
|
|
405
496
|
* the harness ran both, and one question produced two notifications.
|
|
406
497
|
*/
|
|
407
498
|
describe('two checkouts', () => {
|
|
408
|
-
const OTHER = '/Users/rafael/
|
|
499
|
+
const OTHER = '/Users/rafael/notifai-public/apps/cli/dist/main.js'
|
|
409
500
|
|
|
410
501
|
function documentWith(command: string): SettingsDocument {
|
|
411
502
|
return { hooks: { Stop: [{ matcher: '*', hooks: [{ type: 'command', command, timeout: 300 }] }] } }
|
|
@@ -416,26 +507,26 @@ describe('two checkouts', () => {
|
|
|
416
507
|
}
|
|
417
508
|
|
|
418
509
|
it('replaces a handler another checkout installed instead of running beside it', () => {
|
|
419
|
-
const existing = documentWith(hookCommand(
|
|
510
|
+
const existing = documentWith(hookCommand('/other/notifai/hook-adapter', 'stop'))
|
|
420
511
|
const merged = mergeHooks(
|
|
421
512
|
existing,
|
|
422
|
-
buildHookConfig({
|
|
513
|
+
buildHookConfig({ adapterPath: ADAPTER }),
|
|
423
514
|
SCRIPT,
|
|
424
515
|
)
|
|
425
516
|
const commands = stopCommands(merged.document)
|
|
426
517
|
expect(commands.filter((c) => c.includes('hook stop'))).toHaveLength(1)
|
|
427
|
-
expect(commands[0]).toContain(
|
|
428
|
-
expect(commands.join(' ')).not.toContain(
|
|
518
|
+
expect(commands[0]).toContain(ADAPTER)
|
|
519
|
+
expect(commands.join(' ')).not.toContain('/other/notifai/hook-adapter')
|
|
429
520
|
})
|
|
430
521
|
|
|
431
522
|
it('uninstalls a handler another checkout installed', () => {
|
|
432
|
-
const removed = removeHooks(documentWith(hookCommand(
|
|
523
|
+
const removed = removeHooks(documentWith(hookCommand('/other/notifai/hook-adapter', 'stop')), SCRIPT)
|
|
433
524
|
expect(stopCommands(removed.document)).toHaveLength(0)
|
|
434
525
|
})
|
|
435
526
|
|
|
436
527
|
it('still recognises an install that predates the marker', () => {
|
|
437
|
-
//
|
|
438
|
-
const legacy = documentWith(`'${EXEC}' '${
|
|
528
|
+
// Different checkout, no marker: the form written by an older build.
|
|
529
|
+
const legacy = documentWith(`'${EXEC}' '${OTHER}' hook stop`)
|
|
439
530
|
expect(stopCommands(removeHooks(legacy, SCRIPT).document)).toHaveLength(0)
|
|
440
531
|
})
|
|
441
532
|
|
|
@@ -443,6 +534,11 @@ describe('two checkouts', () => {
|
|
|
443
534
|
const foreign = documentWith('/usr/local/bin/my-own-thing --stop')
|
|
444
535
|
expect(stopCommands(removeHooks(foreign, SCRIPT).document)).toHaveLength(1)
|
|
445
536
|
})
|
|
537
|
+
|
|
538
|
+
it("preserves a foreign project's generic dist/main.js hook", () => {
|
|
539
|
+
const foreign = documentWith("'/usr/local/bin/node' '/opt/foreign/dist/main.js' hook stop")
|
|
540
|
+
expect(stopCommands(removeHooks(foreign, SCRIPT).document)).toHaveLength(1)
|
|
541
|
+
})
|
|
446
542
|
})
|
|
447
543
|
|
|
448
544
|
/**
|
|
@@ -451,8 +547,7 @@ describe('two checkouts', () => {
|
|
|
451
547
|
*/
|
|
452
548
|
describe('the OpenCode adapter', () => {
|
|
453
549
|
const source = opencodePluginSource({
|
|
454
|
-
|
|
455
|
-
scriptPath: SCRIPT,
|
|
550
|
+
adapterPath: ADAPTER,
|
|
456
551
|
timeoutSeconds: 240,
|
|
457
552
|
})
|
|
458
553
|
|
|
@@ -460,8 +555,9 @@ describe('the OpenCode adapter', () => {
|
|
|
460
555
|
// The whole point: presence, escalation and retirement stay in the CLI.
|
|
461
556
|
expect(source).toContain('"hook", event')
|
|
462
557
|
expect(source).toContain('"--harness", "opencode"')
|
|
463
|
-
expect(source).toContain(JSON.stringify(
|
|
464
|
-
expect(source).toContain(JSON.stringify(
|
|
558
|
+
expect(source).toContain(JSON.stringify(ADAPTER))
|
|
559
|
+
expect(source).not.toContain(JSON.stringify(SCRIPT))
|
|
560
|
+
expect(source).not.toContain(JSON.stringify(EXEC))
|
|
465
561
|
})
|
|
466
562
|
|
|
467
563
|
it('wires the three question lifecycle joints and leaves permissions alone', () => {
|
|
@@ -504,27 +600,21 @@ describe('the OpenCode adapter', () => {
|
|
|
504
600
|
expect(settingsFile('opencode', false, '/repo', {})).toContain('notifai.js')
|
|
505
601
|
})
|
|
506
602
|
|
|
507
|
-
it('reports the
|
|
508
|
-
// `doctor` asks every handler which build it invokes. A plugin is a module
|
|
509
|
-
// rather than a command line, so answering with the plugin's own path made
|
|
510
|
-
// an ordinary OpenCode install look like a second checkout and turned the
|
|
511
|
-
// duplicate check red on a healthy machine.
|
|
603
|
+
it('reports the same stable adapter identity as command-hook harnesses', () => {
|
|
512
604
|
expect(opencodePluginTarget(source)).toEqual({
|
|
513
|
-
|
|
514
|
-
script: SCRIPT,
|
|
605
|
+
adapter: ADAPTER,
|
|
515
606
|
current: true,
|
|
516
607
|
timeoutSeconds: 240,
|
|
517
608
|
})
|
|
518
609
|
expect(opencodePluginTarget(source.replace(/^const ADAPTER_VERSION = .*\n/m, ''))).toEqual({
|
|
519
|
-
|
|
520
|
-
script: SCRIPT,
|
|
610
|
+
adapter: ADAPTER,
|
|
521
611
|
current: false,
|
|
522
612
|
timeoutSeconds: 240,
|
|
523
613
|
})
|
|
524
614
|
expect(opencodePluginTarget('export const SomeoneElse = () => ({})')).toBeNull()
|
|
525
615
|
// Ours, but with the constants edited beyond recognition: no answer beats
|
|
526
616
|
// a wrong one, since the caller compares these for equality.
|
|
527
|
-
expect(opencodePluginTarget(`${OPENCODE_PLUGIN_MARKER}\nconst
|
|
617
|
+
expect(opencodePluginTarget(`${OPENCODE_PLUGIN_MARKER}\nconst ADAPTER = whatever\n`)).toBeNull()
|
|
528
618
|
})
|
|
529
619
|
})
|
|
530
620
|
|