Skip to content

Commit 2e6f245

Browse files
IM.codesclaude
andcommitted
test: add 9 tests for system-injected message filtering
Covers: task-notification, system-reminder, command-name, command-message, local-command-caveat, bash-input/stdout/stderr, string-form content. Verifies normal user messages and natural mentions of XML tags pass through. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 051fa36 commit 2e6f245

File tree

1 file changed

+159
-0
lines changed

1 file changed

+159
-0
lines changed

test/daemon/jsonl-watcher.test.ts

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,3 +734,162 @@ describe('progress event subtypes', () => {
734734
expect(events.length).toBeGreaterThanOrEqual(1);
735735
});
736736
});
737+
738+
// ── System-injected message filtering ──────────────────────────────────────
739+
740+
describe('system-injected message filtering', () => {
741+
it('filters <task-notification> and emits agent.status processing instead', async () => {
742+
const filePath = join(testDir, 'test.jsonl');
743+
await writeFile(filePath, '');
744+
await startWatchingFile('test_session', filePath);
745+
await new Promise((r) => setTimeout(r, 200));
746+
747+
await appendFile(filePath, jsonlLine({
748+
type: 'user',
749+
timestamp: new Date().toISOString(),
750+
message: { content: [{ type: 'text', text: '<task-notification>\n<task-id>abc123</task-id>\n<status>completed</status>\n<summary>Background command done</summary>\n</task-notification>' }] },
751+
}));
752+
await new Promise((r) => setTimeout(r, 2500));
753+
754+
// Should NOT appear as user.message
755+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('task-notification'));
756+
expect(userMsgs).toHaveLength(0);
757+
758+
// Should emit agent.status processing
759+
const statusEvents = emittedEvents.filter((e) => e.type === 'agent.status' && e.payload.status === 'processing');
760+
expect(statusEvents.length).toBeGreaterThanOrEqual(1);
761+
});
762+
763+
it('filters <system-reminder> and emits agent.status processing instead', async () => {
764+
const filePath = join(testDir, 'test.jsonl');
765+
await writeFile(filePath, '');
766+
await startWatchingFile('test_session', filePath);
767+
await new Promise((r) => setTimeout(r, 200));
768+
769+
await appendFile(filePath, jsonlLine({
770+
type: 'user',
771+
timestamp: new Date().toISOString(),
772+
message: { content: [{ type: 'text', text: '<system-reminder>\nThe following tools are available...\n</system-reminder>' }] },
773+
}));
774+
await new Promise((r) => setTimeout(r, 2500));
775+
776+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('system-reminder'));
777+
expect(userMsgs).toHaveLength(0);
778+
779+
const statusEvents = emittedEvents.filter((e) => e.type === 'agent.status' && e.payload.status === 'processing');
780+
expect(statusEvents.length).toBeGreaterThanOrEqual(1);
781+
});
782+
783+
it('filters <command-name> slash commands', async () => {
784+
const filePath = join(testDir, 'test.jsonl');
785+
await writeFile(filePath, '');
786+
await startWatchingFile('test_session', filePath);
787+
await new Promise((r) => setTimeout(r, 200));
788+
789+
await appendFile(filePath, jsonlLine({
790+
type: 'user',
791+
timestamp: new Date().toISOString(),
792+
message: { content: [{ type: 'text', text: '<command-name>/commit</command-name>\n<command-message>commit</command-message>\n<command-args></command-args>' }] },
793+
}));
794+
await new Promise((r) => setTimeout(r, 2500));
795+
796+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('command-name'));
797+
expect(userMsgs).toHaveLength(0);
798+
});
799+
800+
it('filters <local-command-caveat> local commands', async () => {
801+
const filePath = join(testDir, 'test.jsonl');
802+
await writeFile(filePath, '');
803+
await startWatchingFile('test_session', filePath);
804+
await new Promise((r) => setTimeout(r, 200));
805+
806+
await appendFile(filePath, jsonlLine({
807+
type: 'user',
808+
timestamp: new Date().toISOString(),
809+
message: { content: [{ type: 'text', text: '<local-command-caveat>Caveat: local command output</local-command-caveat>\n<local-command-stdout>Model set to opus</local-command-stdout>' }] },
810+
}));
811+
await new Promise((r) => setTimeout(r, 2500));
812+
813+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('local-command'));
814+
expect(userMsgs).toHaveLength(0);
815+
});
816+
817+
it('filters <bash-input> / <bash-stdout> / <bash-stderr>', async () => {
818+
const filePath = join(testDir, 'test.jsonl');
819+
await writeFile(filePath, '');
820+
await startWatchingFile('test_session', filePath);
821+
await new Promise((r) => setTimeout(r, 200));
822+
823+
await appendFile(filePath, jsonlLine({
824+
type: 'user',
825+
timestamp: new Date().toISOString(),
826+
message: { content: [{ type: 'text', text: '<bash-input>ls -la</bash-input>\n<bash-stdout>total 42\n</bash-stdout>' }] },
827+
}));
828+
await new Promise((r) => setTimeout(r, 2500));
829+
830+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('bash-input'));
831+
expect(userMsgs).toHaveLength(0);
832+
});
833+
834+
it('filters <command-message> tags', async () => {
835+
const filePath = join(testDir, 'test.jsonl');
836+
await writeFile(filePath, '');
837+
await startWatchingFile('test_session', filePath);
838+
await new Promise((r) => setTimeout(r, 200));
839+
840+
await appendFile(filePath, jsonlLine({
841+
type: 'user',
842+
timestamp: new Date().toISOString(),
843+
message: { content: [{ type: 'text', text: '<command-message>model</command-message>' }] },
844+
}));
845+
await new Promise((r) => setTimeout(r, 2500));
846+
847+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('command-message'));
848+
expect(userMsgs).toHaveLength(0);
849+
});
850+
851+
it('filters string-form user content with system tags', async () => {
852+
const filePath = join(testDir, 'test.jsonl');
853+
await writeFile(filePath, '');
854+
await startWatchingFile('test_session', filePath);
855+
await new Promise((r) => setTimeout(r, 200));
856+
857+
// String content (not array blocks) — real CC format
858+
await appendFile(filePath, jsonlLine({
859+
type: 'user',
860+
timestamp: new Date().toISOString(),
861+
message: { role: 'user', content: '<system-reminder>\nToday is 2026-03-20.\n</system-reminder>' },
862+
}));
863+
await new Promise((r) => setTimeout(r, 2500));
864+
865+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('system-reminder'));
866+
expect(userMsgs).toHaveLength(0);
867+
});
868+
869+
it('does NOT filter normal user messages', async () => {
870+
const filePath = join(testDir, 'test.jsonl');
871+
await writeFile(filePath, '');
872+
await startWatchingFile('test_session', filePath);
873+
await new Promise((r) => setTimeout(r, 200));
874+
875+
await appendFile(filePath, userMessage('Hello, please review this code'));
876+
await new Promise((r) => setTimeout(r, 2500));
877+
878+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && e.payload.text === 'Hello, please review this code');
879+
expect(userMsgs.length).toBeGreaterThanOrEqual(1);
880+
});
881+
882+
it('does NOT filter user messages that mention XML tags in natural text', async () => {
883+
const filePath = join(testDir, 'test.jsonl');
884+
await writeFile(filePath, '');
885+
await startWatchingFile('test_session', filePath);
886+
await new Promise((r) => setTimeout(r, 200));
887+
888+
// User talks ABOUT task-notification but it's not a real system inject
889+
await appendFile(filePath, userMessage('What does the task-notification XML look like?'));
890+
await new Promise((r) => setTimeout(r, 2500));
891+
892+
const userMsgs = emittedEvents.filter((e) => e.type === 'user.message' && String(e.payload.text).includes('task-notification'));
893+
expect(userMsgs.length).toBeGreaterThanOrEqual(1);
894+
});
895+
});

0 commit comments

Comments
 (0)