From 07912f02c14dd5e77a5758ae6cd69b4b9db96159 Mon Sep 17 00:00:00 2001 From: Ross Gardler Date: Sun, 18 Jan 2026 00:52:25 -0800 Subject: [PATCH] ge-hch.5.15.19: add coverage for player preference tracker --- .beads/issues.jsonl | 2 +- tests/unit/player-preference.test.js | 32 +++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/.beads/issues.jsonl b/.beads/issues.jsonl index ca7d030..40563f8 100644 --- a/.beads/issues.jsonl +++ b/.beads/issues.jsonl @@ -147,7 +147,7 @@ {"id":"ge-hch.5.15.16","title":"Implement: Embedding Service","description":"Create web/demo/js/embedding-service.js with transformers.js.\n\n## Acceptance Criteria\n- [ ] Loads Xenova/all-MiniLM-L6-v2 model via transformers.js\n- [ ] WebWorker wrapper for non-blocking inference\n- [ ] embed(text) returns embedding vector\n- [ ] similarity(vec1, vec2) returns cosine similarity\n- [ ] Lazy loading on first use\n- [ ] Graceful fallback if model fails\n\n## Related Feature\nge-hch.5.15.4 (Embedding Service)","status":"closed","priority":2,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:41.761163209-08:00","created_by":"rgardler","updated_at":"2026-01-17T20:15:52.375357302-08:00","closed_at":"2026-01-17T20:15:52.375357302-08:00","close_reason":"Completed via PR #170","external_ref":"gh-170","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.16","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:41.761957697-08:00","created_by":"rgardler"}],"comments":[{"id":212,"issue_id":"ge-hch.5.15.16","author":"rgardler","text":"Opened PR https://github.com/TheWizardsCode/GEngine/pull/170 for embedding service. Added web/demo/js/embedding-service.js: lazy loads Xenova/all-MiniLM-L6-v2 via transformers.js inside a Web Worker; provides embed(text)-\u003eembedding (null on failure) and similarity(vecA, vecB). Graceful fallback when workers/transformers unavailable. Tests run: npx jest tests/unit/director.test.js --runInBand (pass).","created_at":"2026-01-18T03:31:14Z"},{"id":213,"issue_id":"ge-hch.5.15.16","author":"rgardler","text":"PR #170 merged. Deleted branch feature/ge-hch.5.15.16-embedding locally and remotely. Closing bead as Completed via PR #170.","created_at":"2026-01-18T04:15:54Z"}]} {"id":"ge-hch.5.15.17","title":"Tests: Embedding Service","description":"Unit tests for embedding service.\n\n## Acceptance Criteria\n- [ ] Test: similarity(happy, joyful) \u003e 0.7\n- [ ] Test: similarity(happy, database) \u003c 0.4\n- [ ] Test: embed(null) returns null gracefully\n- [ ] Performance: first embed \u003c 3s, subsequent \u003c 100ms\n\n## Related Feature\nge-hch.5.15.4 (Embedding Service)","status":"closed","priority":2,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:41.806727691-08:00","created_by":"rgardler","updated_at":"2026-01-17T20:41:48.959573685-08:00","closed_at":"2026-01-17T20:41:48.959573685-08:00","close_reason":"Completed","external_ref":"gh-171","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.17","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:41.807448395-08:00","created_by":"rgardler"}],"comments":[{"id":214,"issue_id":"ge-hch.5.15.17","author":"rgardler","text":"PR https://github.com/TheWizardsCode/GEngine/pull/171 opened. Added deterministic unit tests for similarity thresholds (happy/joyful \u003e 0.7, happy/database \u003c 0.4), embed(null) fallback, and mocked worker plumbing for embed(). Optional gated integration test (INTEGRATION_EMBEDDING=1) included for real model check. Tests run: npx jest tests/unit/embedding.service.similarity.test.js tests/unit/embedding.service.embed.test.js --runInBand; npx jest tests/unit/director.test.js --runInBand.","created_at":"2026-01-18T04:19:41Z"}]} {"id":"ge-hch.5.15.18","title":"Implement: Player Preference Tracker","description":"Create web/demo/js/player-preference.js for tracking preferences.\n\n## Acceptance Criteria\n- [ ] Records { branchType, accepted, timestamp } events\n- [ ] Computes preference score per branch type (0.0-1.0)\n- [ ] Persists in localStorage key ge-hch.ai-preferences\n- [ ] Cold-start returns 0.5 for all types\n- [ ] getPreference(branchType) and recordOutcome(branchType, accepted) APIs\n\n## Related Feature\nge-hch.5.15.5 (Player Preference Tracker)","status":"closed","priority":2,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:51.748963075-08:00","created_by":"rgardler","updated_at":"2026-01-18T00:42:47.180670606-08:00","closed_at":"2026-01-18T00:42:47.180670606-08:00","close_reason":"Completed","external_ref":"https://github.com/TheWizardsCode/GEngine/pull/172","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.18","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:51.750476216-08:00","created_by":"rgardler"}]} -{"id":"ge-hch.5.15.19","title":"Tests: Player Preference Tracker","description":"Unit tests for player preference tracking.\n\n## Acceptance Criteria\n- [ ] Test: 3 accepts + 1 reject of dialogue yields preference \u003e 0.6\n- [ ] Test: 0 history yields preference = 0.5\n- [ ] Test: 100+ events still performant (\u003c10ms)\n- [ ] Test: localStorage persistence works\n\n## Related Feature\nge-hch.5.15.5 (Player Preference Tracker)","status":"open","priority":2,"issue_type":"task","assignee":"Probe","created_at":"2026-01-16T15:03:51.807524607-08:00","created_by":"rgardler","updated_at":"2026-01-16T15:03:51.807524607-08:00","dependencies":[{"issue_id":"ge-hch.5.15.19","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:51.808421437-08:00","created_by":"rgardler"}]} +{"id":"ge-hch.5.15.19","title":"Tests: Player Preference Tracker","description":"Unit tests for player preference tracking.\n\n## Acceptance Criteria\n- [ ] Test: 3 accepts + 1 reject of dialogue yields preference \u003e 0.6\n- [ ] Test: 0 history yields preference = 0.5\n- [ ] Test: 100+ events still performant (\u003c10ms)\n- [ ] Test: localStorage persistence works\n\n## Related Feature\nge-hch.5.15.5 (Player Preference Tracker)","status":"in_progress","priority":2,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:51.807524607-08:00","created_by":"rgardler","updated_at":"2026-01-18T00:51:57.268187359-08:00","dependencies":[{"issue_id":"ge-hch.5.15.19","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:51.808421437-08:00","created_by":"rgardler"}]} {"id":"ge-hch.5.15.2","title":"Return-Path Feasibility Checker","description":"Validate that the AI's proposed return knot exists in the story to prevent dead-ends.\n\n## Player Experience Change\nPlayers will never be stranded in an AI branch with no way back. If the AI proposes a non-existent return path, the choice is silently rejected.\n\n## Acceptance Criteria\n- [ ] Returns `{ feasible: boolean, reason: string, confidence: number }`\n- [ ] `feasible=true` if `return_path` knot exists in story (confidence=0.9)\n- [ ] `feasible=false` if knot does not exist (confidence=0.0, reason='Return path knot does not exist')\n- [ ] Completes in \u003c50ms\n- [ ] Unit test: `return_path: 'campfire'` passes (knot exists in demo.ink)\n- [ ] Unit test: `return_path: 'nonexistent_knot_xyz'` fails\n- [ ] Integration test: Director rejects proposal with invalid return_path\n\n## Minimal Implementation\n- Create `checkReturnPath(returnPath, story)` function\n- Extract knot names from `story.mainContentContainer._namedContent`\n- Simple existence check\n\n## Dependencies\n- ge-hch.5.15.1 (Decision Flow Engine)\n\n## Deliverables\n- Return-path checker in director.js\n- Unit tests with valid/invalid return paths","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-16T15:01:40.467783504-08:00","created_by":"rgardler","updated_at":"2026-01-17T10:51:48.6478971-08:00","closed_at":"2026-01-17T10:51:48.6478971-08:00","close_reason":"Return-path checker implemented, tested and integrated into Director","dependencies":[{"issue_id":"ge-hch.5.15.2","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:01:40.469157452-08:00","created_by":"rgardler"},{"issue_id":"ge-hch.5.15.2","depends_on_id":"ge-hch.5.15.1","type":"blocks","created_at":"2026-01-16T15:04:32.206416228-08:00","created_by":"rgardler"}]} {"id":"ge-hch.5.15.20","title":"Implement: Director Integration","description":"Modify inkrunner.js to use Director for AI choice injection.\n\n## Acceptance Criteria\n- [ ] generateAIChoice() calls director.evaluate() before injecting\n- [ ] AI choice injected only if decision === approve\n- [ ] Silent skip on reject (no error, no AI choice)\n- [ ] Loading indicator shows Evaluating AI choice during evaluation\n- [ ] Logs rejection reasons to console\n\n## Implementation Notes\n- Modify generateAIChoice() in web/demo/js/inkrunner.js\n- Import director.js module\n- Handle both sync and async evaluation\n\n## Related Feature\nge-hch.5.15.6 (Director Integration)","status":"closed","priority":1,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:59.856948737-08:00","created_by":"rgardler","updated_at":"2026-01-17T19:07:33.698779843-08:00","closed_at":"2026-01-17T19:07:33.698779843-08:00","close_reason":"Completed","external_ref":"https://github.com/TheWizardsCode/GEngine/pull/167","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.20","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:59.857773656-08:00","created_by":"rgardler"}],"comments":[{"id":207,"issue_id":"ge-hch.5.15.20","author":"rgardler","text":"Integrated Director evaluate into addAIChoice with sync/async support; added require fallback for Director in Node. Loading indicator now shows 'Evaluating AI choice...' during Director evaluation. Rejects are silent with console reason; approvals inject AI choice. Tests updated for sync evaluate; targeted jest run: npx jest tests/unit/inkrunner.test.js tests/unit/director.test.js --runInBand (pass).","created_at":"2026-01-18T03:03:41Z"}]} {"id":"ge-hch.5.15.21","title":"Tests: Director Integration","description":"Integration tests for Director-governed injection.\n\n## Acceptance Criteria\n- [ ] Playthrough: complete demo.ink with mix of accepted/rejected\n- [ ] Playthrough: no runtime errors when all branches rejected\n- [ ] Test: mocked Director approve leads to AI choice shown\n- [ ] Test: mocked Director reject leads to no AI choice\n\n## Related Feature\nge-hch.5.15.6 (Director Integration)","status":"closed","priority":1,"issue_type":"task","assignee":"@Patch","created_at":"2026-01-16T15:03:59.901304347-08:00","created_by":"rgardler","updated_at":"2026-01-17T19:17:35.645282782-08:00","closed_at":"2026-01-17T19:17:35.645282782-08:00","close_reason":"Completed","external_ref":"https://github.com/TheWizardsCode/GEngine/pull/168","labels":["Status: PR Created"],"dependencies":[{"issue_id":"ge-hch.5.15.21","depends_on_id":"ge-hch.5.15","type":"parent-child","created_at":"2026-01-16T15:03:59.902099293-08:00","created_by":"rgardler"}]} diff --git a/tests/unit/player-preference.test.js b/tests/unit/player-preference.test.js index 49592d5..6fb9fef 100644 --- a/tests/unit/player-preference.test.js +++ b/tests/unit/player-preference.test.js @@ -6,13 +6,28 @@ function clearStorage() { } } +function corruptStorage() { + if (typeof localStorage !== 'undefined') { + localStorage.setItem('ge-hch.ai-preferences', '{ not valid json'); + } +} + describe('PlayerPreference', () => { beforeEach(() => { clearStorage(); }); test('cold start returns default 0.5', () => { - expect(PlayerPreference.getPreference('combat')).toBe(0.5); + expect(PlayerPreference.getPreference('dialogue')).toBe(0.5); + }); + + test('3 accepts + 1 reject yields > 0.6 for dialogue', () => { + PlayerPreference.recordOutcome('dialogue', true); + PlayerPreference.recordOutcome('dialogue', true); + PlayerPreference.recordOutcome('dialogue', true); + const score = PlayerPreference.recordOutcome('dialogue', false); + expect(score).toBeGreaterThan(0.6); // 0.75 expected + expect(PlayerPreference.getPreference('dialogue')).toBeGreaterThan(0.6); }); test('records outcomes and updates preference', () => { @@ -34,10 +49,17 @@ describe('PlayerPreference', () => { test('clamps to 0-1 and handles bad inputs', () => { expect(PlayerPreference.getPreference('')).toBe(0.5); expect(PlayerPreference.recordOutcome('', true)).toBe(0.5); - // Corrupt storage - if (typeof localStorage !== 'undefined') { - localStorage.setItem('ge-hch.ai-preferences', '{ not valid json'); - } + corruptStorage(); expect(PlayerPreference.getPreference('combat')).toBe(0.5); }); + + test('handles 100+ events quickly', () => { + const start = performance.now(); + for (let i = 0; i < 100; i++) { + PlayerPreference.recordOutcome('exploration', i % 2 === 0); + } + const elapsed = performance.now() - start; + expect(PlayerPreference.getPreference('exploration')).toBeGreaterThanOrEqual(0); + expect(elapsed).toBeLessThan(10); + }); });