Initial import
diff --git a/test/data/gender.conllu b/test/data/gender.conllu
new file mode 100644
index 0000000..bc3c76c
--- /dev/null
+++ b/test/data/gender.conllu
@@ -0,0 +1,117 @@
+# foundry = base
+# filename = TEST/gender/000001/base/tokens.xml
+# text_id = GENDER_TEST.000001
+# text = Im Prinzip kann jede*r Bürger*in die Umbenennung beantragen
+1	Im	_	ADP	APPRART	_	_	_	_	_
+2	Prinzip	_	NOUN	NN	_	_	_	_	_
+3	kann	_	AUX	VMFIN	_	_	_	_	_
+4	jede*r	_	DET	PIAT	_	_	_	_	_
+5	Bürger*in	_	NOUN	NN	_	_	_	_	_
+6	die	_	DET	ART	_	_	_	_	_
+7	Umbenennung	_	NOUN	NN	_	_	_	_	_
+8	beantragen	_	VERB	VVINF	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000002/base/tokens.xml
+# text_id = GENDER_TEST.000002
+# text = Die Tests sind freiwillig, jede*r SchülerIn hat Anspruch auf kostenlose Tests
+1	Die	_	DET	ART	_	_	_	_	_
+2	Tests	_	NOUN	NN	_	_	_	_	_
+3	sind	_	AUX	VAFIN	_	_	_	_	_
+4	freiwillig	_	ADJ	ADJD	_	_	_	_	_
+5	,	_	PUNCT	$,	_	_	_	_	_
+6	jede*r	_	DET	PIAT	_	_	_	_	_
+7	SchülerIn	_	NOUN	NN	_	_	_	_	_
+8	hat	_	AUX	VAFIN	_	_	_	_	_
+9	Anspruch	_	NOUN	NN	_	_	_	_	_
+10	auf	_	ADP	APPR	_	_	_	_	_
+11	kostenlose	_	ADJ	ADJA	_	_	_	_	_
+12	Tests	_	NOUN	NN	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000003/base/tokens.xml
+# text_id = GENDER_TEST.000003
+# text = Als Kinder- und Jugendpsychiater*in ist sie bekannt
+1	Als	_	ADP	APPR	_	_	_	_	_
+2	Kinder-	_	NOUN	NN	_	_	_	_	_
+3	und	_	CCONJ	KON	_	_	_	_	_
+4	Jugendpsychiater*in	_	_	_	_	_	_	_	_
+5	ist	_	AUX	VAFIN	_	_	_	_	_
+6	sie	_	PRON	PPER	_	_	_	_	_
+7	bekannt	_	ADJ	ADJD	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000004/base/tokens.xml
+# text_id = GENDER_TEST.000004
+# text = Wir suchen eine*n begeisterte*n Nachfolger*in, als Anhänger:in linker Ideen
+1	Wir	_	PRON	PPER	_	_	_	_	_
+2	suchen	_	VERB	VVFIN	_	_	_	_	_
+3	eine*n	_	DET	ART	_	_	_	_	_
+4	begeisterte*n	_	ADJ	ADJA	_	_	_	_	_
+5	Nachfolger*in	_	_	_	_	_	_	_	_
+6	,	_	PUNCT	$,	_	_	_	_	_
+7	als	_	ADP	APPR	_	_	_	_	_
+8	Anhänger:in	_	_	_	_	_	_	_	_
+9	linker	_	ADJ	ADJA	_	_	_	_	_
+10	Ideen	_	NOUN	NN	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000005/base/tokens.xml
+# text_id = GENDER_TEST.000005
+# text = Fachärzt*innen, Lehrer:innen und Autor_innen schreiben
+1	Fachärzt*innen	_	_	_	_	_	_	_	_
+2	,	_	PUNCT	$,	_	_	_	_	_
+3	Lehrer:innen	_	_	_	_	_	_	_	_
+4	und	_	CCONJ	KON	_	_	_	_	_
+5	Autor_innen	_	_	_	_	_	_	_	_
+6	schreiben	_	VERB	VVFIN	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000006/base/tokens.xml
+# text_id = GENDER_TEST.000006
+# text = LehrerInnen, Schüler(innen) und Autor/innen lesen
+1	LehrerInnen	_	_	_	_	_	_	_	_
+2	,	_	PUNCT	$,	_	_	_	_	_
+3	Schüler(innen)	_	_	_	_	_	_	_	_
+4	und	_	CCONJ	KON	_	_	_	_	_
+5	Autor/innen	_	_	_	_	_	_	_	_
+6	lesen	_	VERB	VVFIN	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000007/base/tokens.xml
+# text_id = GENDER_TEST.000007
+# text = die*der Antragssteller*in schreibt jede:r Wirt:in
+1	die*der	_	DET	ART	_	_	_	_	_
+2	Antragssteller*in	_	_	_	_	_	_	_	_
+3	schreibt	_	VERB	VVFIN	_	_	_	_	_
+4	jede:r	_	DET	PIAT	_	_	_	_	_
+5	Wirt:in	_	_	_	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000008/base/tokens.xml
+# text_id = GENDER_TEST.000008
+# text = sie*er schrieb Menschenrechtsanwält:innen
+1	sie*er	_	_	_	_	_	_	_	_
+2	schrieb	_	VERB	VVFIN	_	_	_	_	_
+3	Menschenrechtsanwält:innen	_	_	_	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000009/base/tokens.xml
+# text_id = GENDER_TEST.000009
+# text = ohne jedEn ZeugIn sprach die Generalstaatsanwält*in
+1	ohne	_	ADP	APPR	_	_	_	_	_
+2	jedEn	_	DET	PIAT	_	_	_	_	_
+3	ZeugIn	_	_	_	_	_	_	_	_
+4	sprach	_	VERB	VVFIN	_	_	_	_	_
+5	die	_	DET	ART	_	_	_	_	_
+6	Generalstaatsanwält*in	_	_	_	_	_	_	_	_
+
+# foundry = base
+# filename = TEST/gender/000010/base/tokens.xml
+# text_id = GENDER_TEST.000010
+# text = Autor/-innen und Spieler/-innen lasen
+1	Autor/-innen	_	_	_	_	_	_	_	_
+2	und	_	CCONJ	KON	_	_	_	_	_
+3	Spieler/-innen	_	_	_	_	_	_	_	_
+4	lasen	_	VERB	VVFIN	_	_	_	_	_
+
diff --git a/test/test.js b/test/test.js
new file mode 100644
index 0000000..6367049
--- /dev/null
+++ b/test/test.js
@@ -0,0 +1,173 @@
+const { execSync } = require('child_process');
+
+describe('conllu-gender', () => {
+
+  test('Full mode: all gender-sensitive nouns (Genderstern singular)', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+
+    // Genderstern singular noun: Bürger*in → lemma Bürger*in, NOUN NN Gender=NonBin|Number=Sing
+    expect(stdout).toContain('Bürger*in\tBürger*in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    // Long compound: Jugendpsychiater*in
+    expect(stdout).toContain('Jugendpsychiater*in\tJugendpsychiater*in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    // Compound with umlaut base: Generalstaatsanwält*in
+    expect(stdout).toContain('Generalstaatsanwält*in\tGeneralstaatsanwält*in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    // Nachfolger*in
+    expect(stdout).toContain('Nachfolger*in\tNachfolger*in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    // With umlaut base: Antragssteller*in (no umlaut but long compound)
+    expect(stdout).toContain('Antragssteller*in\tAntragssteller*in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+  });
+
+  test('Full mode: Genderstern plural nouns → lemma uses singular, Number=Plur', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // Fachärzt*innen → lemma Fachärzt*in
+    expect(stdout).toContain('Fachärzt*innen\tFachärzt*in\tNOUN\tNN\tGender=NonBin|Number=Plur');
+  });
+
+  test('Full mode: Doppelpunkt nouns', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // Doppelpunkt singular
+    expect(stdout).toContain('Anhänger:in\tAnhänger:in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    expect(stdout).toContain('Wirt:in\tWirt:in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    // Doppelpunkt plural  
+    expect(stdout).toContain('Lehrer:innen\tLehrer:in\tNOUN\tNN\tGender=NonBin|Number=Plur');
+    // Long compound with umlaut base
+    expect(stdout).toContain('Menschenrechtsanwält:innen\tMenschenrechtsanwält:in\tNOUN\tNN\tGender=NonBin|Number=Plur');
+  });
+
+  test('Full mode: Unterstrich plural nouns', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    expect(stdout).toContain('Autor_innen\tAutor_in\tNOUN\tNN\tGender=NonBin|Number=Plur');
+  });
+
+  test('Full mode: Binnen-I nouns (binary intended → Gender=Masc,Fem)', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // Binnen-I singular: ZeugIn → Gender=Masc,Fem|Number=Sing
+    expect(stdout).toContain('ZeugIn\tZeugIn\tNOUN\tNN\tGender=Masc,Fem|Number=Sing');
+    // Binnen-I singular: SchülerIn → Gender=Masc,Fem|Number=Sing
+    expect(stdout).toContain('SchülerIn\tSchülerIn\tNOUN\tNN\tGender=Masc,Fem|Number=Sing');
+    // Binnen-I plural: LehrerInnen → lemma LehrerIn, Number=Plur
+    expect(stdout).toContain('LehrerInnen\tLehrerIn\tNOUN\tNN\tGender=Masc,Fem|Number=Plur');
+  });
+
+  test('Full mode: Klammern plural nouns (binary intended)', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    expect(stdout).toContain('Schüler(innen)\tSchüler(in)\tNOUN\tNN\tGender=Masc,Fem|Number=Plur');
+  });
+
+  test('Full mode: Schrägstrich nouns', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // Simple slash
+    expect(stdout).toContain('Autor/innen\tAutor/in\tNOUN\tNN\tGender=Masc,Fem|Number=Plur');
+    // Slash with Ergänzungsstrich (/-innen)
+    expect(stdout).toContain('Autor/-innen\tAutor/in\tNOUN\tNN\tGender=Masc,Fem|Number=Plur');
+    expect(stdout).toContain('Spieler/-innen\tSpieler/in\tNOUN\tNN\tGender=Masc,Fem|Number=Plur');
+  });
+
+  test('Full mode: gendered determiners/pronouns (non-binary intended)', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // jede*r → DET PIAT Gender=NonBin, lemma jede*r
+    expect(stdout).toContain('jede*r\tjede*r\tDET\tPIAT\tGender=NonBin');
+    const jeder_count = (stdout.match(/jede\*r\tjede\*r/g) || []).length;
+    expect(jeder_count).toBe(2);  // appears in two sentences
+    // eine*n → DET ART Gender=NonBin  (non-binary via *)
+    expect(stdout).toContain('eine*n\teine*n\tDET\tART\tGender=NonBin');
+    // jede:r → DET PIAT Gender=NonBin
+    expect(stdout).toContain('jede:r\tjede:r\tDET\tPIAT\tGender=NonBin');
+    // die*der → DET ART Gender=NonBin  (* marker → NonBin even for merged forms)
+    expect(stdout).toContain('die*der\tdie*der\tDET\tART\tGender=NonBin');
+  });
+
+  test('Full mode: neo-pronoun (sie*er → PRON PPER Gender=NonBin)', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    expect(stdout).toContain('sie*er\tsie*er\tPRON\tPPER\tGender=NonBin');
+  });
+
+  test('Full mode: foundry comment changed to gender', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    const foundry_count = (stdout.match(/# foundry = gender/g) || []).length;
+    expect(foundry_count).toBe(10);
+  });
+
+  test('Full mode: non-gender tokens pass through unchanged', () => {
+    const command = 'node src/index.js < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // Regular nouns are passed through, lemma stays _
+    expect(stdout).toContain('Umbenennung\t_\tNOUN\tNN');
+    expect(stdout).toContain('Ideen\t_\tNOUN\tNN');
+  });
+
+  test('Sparse mode: only annotated tokens are emitted', () => {
+    const command = 'node src/index.js -s < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    const lines = stdout.split('\n');
+    const tokenLines = lines.filter(l => l.match(/^\d+\t/));
+    // Every token line must carry a gender annotation:
+    // either the lemma column (col 2) is non-underscore (noun/pron was annotated)
+    // or the features column (col 5) is non-underscore (det was annotated)
+    tokenLines.forEach(line => {
+      const cols = line.split('\t');
+      const lemmaAnnotated  = cols[2] !== '_';
+      const featsAnnotated  = cols[5] !== '_';
+      expect(lemmaAnnotated || featsAnnotated).toBe(true);
+    });
+    // Count: 18 NOUN + 5 DET + 1 PRON = 24 annotated tokens
+    expect(tokenLines.length).toBe(24);
+  });
+
+  test('Sparse mode: sentence headers are emitted for sentences with matches', () => {
+    const command = 'node src/index.js -s < test/data/gender.conllu';
+    const stdout = execSync(command).toString();
+    // All 10 test sentences have at least one gender form
+    const text_id_count = (stdout.match(/# text_id = /g) || []).length;
+    expect(text_id_count).toBe(10);
+  });
+
+  test('Inline input: basic Genderstern annotation', () => {
+    const testInput = `# foundry = base
+# text_id = inline-001
+# text = Die Lehrerin und Lehrer*innen kamen
+1\tDie\t_\tDET\tART\t_\t_\t_\t_\t_
+2\tLehrerin\t_\tNOUN\tNN\t_\t_\t_\t_\t_
+3\tund\t_\tCCONJ\tKON\t_\t_\t_\t_\t_
+4\tLehrer*innen\t_\t_\t_\t_\t_\t_\t_\t_
+5\tkamen\t_\tVERB\tVVFIN\t_\t_\t_\t_\t_
+
+`;
+    const stdout = execSync('node src/index.js', { input: testInput }).toString();
+    expect(stdout).toContain('Lehrer*innen\tLehrer*in\tNOUN\tNN\tGender=NonBin|Number=Plur');
+    // Regular noun 'Lehrerin' must not be incorrectly tagged
+    expect(stdout).toContain('Lehrerin\t_\tNOUN\tNN\t_');
+  });
+
+  test('Inline input: existing POS/lemma/feats are replaced for gender forms', () => {
+    const testInput = `# foundry = base
+# text_id = inline-002
+# text = jede Ärztin und jede*r Arzt*in
+1\tjede\t_\tDET\tPIAT\tGender=Fem|Number=Sing\t_\t_\t_\t_
+2\tÄrztin\tÄrztin\tNOUN\tNN\tGender=Fem|Number=Sing\t_\t_\t_\t_
+3\tund\t_\tCCONJ\tKON\t_\t_\t_\t_\t_
+4\tjede*r\t_\tDET\tPIAT\t_\t_\t_\t_\t_
+5\tArzt*in\t_\t_\t_\t_\t_\t_\t_\t_
+
+`;
+    const stdout = execSync('node src/index.js', { input: testInput }).toString();
+    // jede*r: missing feats should be filled in
+    expect(stdout).toContain('jede*r\tjede*r\tDET\tPIAT\tGender=NonBin');
+    // Arzt*in: umlaut base, missing everything
+    expect(stdout).toContain('Arzt*in\tArzt*in\tNOUN\tNN\tGender=NonBin|Number=Sing');
+    // Ärztin (regular moviertes Femininum, no gender marker): unchanged
+    expect(stdout).toContain('Ärztin\tÄrztin\tNOUN\tNN\tGender=Fem|Number=Sing');
+    // jede (without gender marker): unchanged
+    expect(stdout).toContain('jede\t_\tDET\tPIAT\tGender=Fem|Number=Sing');
+  });
+});