my harness for niri
1import assert from "node:assert/strict"
2import test from "node:test"
3import { __loopTest } from "./loop.js"
4import type { Message } from "../types.js"
5import type { LoopState } from "./types.js"
6import type { Message } from "../types.js"
7
8function makeState(): LoopState {
9 return {
10 conversation: [],
11 pendingInputs: [],
12 tokenCount: 0,
13 contextSize: 0,
14 toolInFlight: false,
15 memoryRecallCooldowns: {},
16 memoryRecallTurn: 0,
17 }
18}
19
20test("applyDiscordSendNudge appends a follow-up user nudge for unsent Discord replies", () => {
21 const state = makeState()
22 const turnMessages: Message[] = [
23 {
24 role: "user",
25 content: "[user/discord] [discord/dm] hey are you there",
26 },
27 {
28 role: "assistant",
29 content: "yeah, i'm here",
30 },
31 ]
32
33 const nudged = __loopTest.applyDiscordSendNudge(state, turnMessages)
34
35 assert.equal(nudged, true)
36 assert.equal(state.conversation.length, 1)
37 assert.match(String(state.conversation[0]?.content), /did not call discord_send/i)
38})
39
40test("applyDiscordSendNudge fires after a harness restart when the discord event is pre-turn context", () => {
41 const state = makeState()
42 state.conversation.push(
43 {
44 role: "user",
45 content: "[harness restarted — discord @ 2026-05-01T04:30:00.000Z]\n\n[discord/dm] hi starfish",
46 },
47 {
48 role: "assistant",
49 content: "i keep getting bounced by harness restarts sorry ^^ still here though! what's up?",
50 },
51 )
52
53 const turnStart = 1
54 const turnMessages: Message[] = [state.conversation[1]!]
55
56 const nudged = __loopTest.applyDiscordSendNudge(state, turnMessages, turnStart)
57
58 assert.equal(nudged, true)
59 assert.equal(state.conversation.length, 3)
60 assert.match(String(state.conversation[2]?.content), /did not call discord_send/i)
61})
62
63test("applyDiscordSendNudge does not fire when discord_send was already called", () => {
64 const state = makeState()
65 const turnMessages: Message[] = [
66 {
67 role: "user",
68 content: "[user/discord] [discord/channel] can you reply",
69 },
70 {
71 role: "assistant",
72 content: "sending now",
73 tool_calls: [
74 {
75 id: "call_123",
76 type: "function",
77 function: {
78 name: "discord_send",
79 arguments: "{\"channel_id\":\"1\",\"content\":\"sending now\"}",
80 },
81 },
82 ],
83 },
84 ]
85
86 const nudged = __loopTest.applyDiscordSendNudge(state, turnMessages)
87
88 assert.equal(nudged, false)
89 assert.equal(state.conversation.length, 0)
90})
91
92test("applyLoopGuardNudge appends an in-band user nudge and saves", async () => {
93 const state = makeState()
94 let saved = false
95
96 await __loopTest.applyLoopGuardNudge(
97 state,
98 {
99 waitForEvent: async () => {
100 throw new Error("unexpected wait")
101 },
102 waitForEventWithTimeout: async () => null,
103 injectIncomingEvent: () => {},
104 flushDeferredEvents: () => {},
105 clearSession: async () => {},
106 saveSession: async () => {
107 saved = true
108 },
109 },
110 "loop guard tripped after 120 turns",
111 )
112
113 assert.equal(saved, true)
114 assert.equal(state.conversation.length, 1)
115 assert.equal(state.conversation[0]?.role, "user")
116 assert.match(String(state.conversation[0]?.content), /loop guard tripped after 120 turns/i)
117})
118
119test("waitForNextEvent waits for and injects the next external event", async () => {
120 const calls: string[] = []
121 const event = {
122 source: "chat" as const,
123 triggeredAt: "2026-05-01T04:40:00.000Z",
124 content: "still here",
125 raw: {},
126 }
127
128 await __loopTest.waitForNextEvent(42, {
129 waitForEvent: async () => {
130 calls.push("wait")
131 return event
132 },
133 waitForEventWithTimeout: async () => null,
134 injectIncomingEvent: (_convId, incoming) => {
135 calls.push(`inject:${incoming.content}`)
136 assert.equal(_convId, 42)
137 assert.equal(incoming, event)
138 },
139 flushDeferredEvents: () => {},
140 clearSession: async () => {},
141 saveSession: async () => {},
142 })
143
144 assert.deepEqual(calls, ["wait", "inject:still here"])
145})