import assert from 'node:assert/strict' import { describe, it } from 'node:test' import { buildPatchWakePrompt, parseGithubRepo, qualifyFriction, qualifyTarget, } from './qualify.ts' describe('parseGithubRepo', () => { it('splits owner/repo', () => { assert.deepEqual(parseGithubRepo('kody-hq/kody'), { owner: 'kody-hq', name: 'kody', }) }) it('rejects empty, package-shaped, and multi-slash values', () => { assert.equal(parseGithubRepo(''), null) assert.equal(parseGithubRepo('@kentcdodds/friction-log'), null) assert.equal(parseGithubRepo('solo'), null) assert.equal(parseGithubRepo('a/b/c'), null) }) }) describe('qualifyTarget', () => { it('requires host github|kody and non-empty repo', () => { assert.equal(qualifyTarget(undefined).ok, false) assert.equal(qualifyTarget({ host: 'gitlab', repo: 'a/b' } as never).ok, false) assert.equal(qualifyTarget({ host: 'github', repo: '' }).ok, false) assert.equal(qualifyTarget({ host: 'github', repo: 'leaf' }).ok, false) assert.equal( qualifyTarget({ host: 'github', repo: 'kody-hq/kody' }).ok, true, ) assert.equal( qualifyTarget({ host: 'kody', repo: '@kentcdodds/friction-log' }).ok, true, ) assert.equal(qualifyTarget({ host: 'kody', repo: 'friction-log' }).ok, true) }) }) describe('qualifyFriction', () => { const goodGithub = { title: 'API returns 500 on empty filter', whatHappened: 'Calling list with empty filter throws 500 instead of empty array', whatYouWanted: 'Stable empty-result contract', howToReproduce: 'POST /list with filter: []', cost: 'Blocks every empty-filter caller', preliminaryInvestigation: 'Read the list handler and empty-filter tests; no root cause claimed.', relevantFiles: ['packages/worker/src/list.ts'], target: { host: 'github' as const, repo: 'kody-hq/kody' }, } it('accepts a durable github papercut', () => { assert.deepEqual(qualifyFriction(goodGithub), { ok: true }) }) it('soft-skips missing preliminaryInvestigation on structured filings', () => { const { preliminaryInvestigation: _drop, relevantFiles: _files, ...rest } = goodGithub const result = qualifyFriction(rest) assert.equal(result.ok, false) if (!result.ok) assert.match(result.reason, /preliminaryInvestigation/) }) it('soft-skips missing target', () => { const result = qualifyFriction({ ...goodGithub, target: undefined, }) assert.equal(result.ok, false) if (!result.ok) assert.match(result.reason, /target is required/) }) it('soft-skips empty github title after Friction: normalize', () => { const result = qualifyFriction({ ...goodGithub, title: 'Friction: ', }) assert.equal(result.ok, false) if (!result.ok) assert.match(result.reason, /title is required/) }) it('soft-skips missing reproducible gap when structured fields are thin', () => { const result = qualifyFriction({ title: 'confused', whatHappened: 'hmm', preliminaryInvestigation: 'Looked at one log line only.', target: { host: 'github', repo: 'kody-hq/kody' }, }) assert.equal(result.ok, false) if (!result.ok) assert.match(result.reason, /reproducible gap|transient/) }) it('soft-skips transient/session-bound heuristic', () => { const result = qualifyFriction({ title: 'agent confused once', whatHappened: 'I got confused briefly', whatYouWanted: 'wanted clarity on one command this session only maybe', preliminaryInvestigation: 'Skimmed one help string this session.', target: { host: 'kody', repo: '@kentcdodds/friction-log' }, }) assert.equal(result.ok, false) if (!result.ok) { assert.match(result.reason, /transient|reproducible/) } }) it('accepts body-only durable report', () => { assert.deepEqual( qualifyFriction({ title: 'Docs omit required target field', body: 'AGENTS.md create example never passed target; agents defaulted wrongly. Repro: follow old snippet.', target: { host: 'github', repo: 'kody-hq/kody' }, }), { ok: true }, ) }) it('does not deny by hardcoded bad titles', () => { const result = qualifyFriction({ title: 'Friction: how do I use this once', whatHappened: 'Following the package README, create rejects without target and error is unclear', whatYouWanted: 'Clear required-field error naming target.host', howToReproduce: 'import create; await create({ title: "x", whatHappened: "y" })', cost: 'Every first-time caller hits this', preliminaryInvestigation: 'Compared create JSDoc with AGENTS.md smoke examples.', target: { host: 'github', repo: 'kody-hq/kody' }, }) assert.deepEqual(result, { ok: true }) }) }) describe('buildPatchWakePrompt', () => { it('includes triage instruction and structured fields', () => { const prompt = buildPatchWakePrompt({ title: 'export map stale', target: { host: 'kody', repo: '@kentcdodds/friction-log' }, whatHappened: 'index still says always files kody-hq/kody', whatYouWanted: 'docs match create contract', howToReproduce: 'read package overview export', cost: 'agents file wrong host', preliminaryInvestigation: 'Opened package overview export and AGENTS.md target section.', relevantFiles: ['src/index.ts', 'AGENTS.md'], }) assert.match(prompt, /Package friction routed to Patch/) assert.match(prompt, /escalate to Cole only if it's platform/) assert.match(prompt, /@kentcdodds\/friction-log/) assert.match(prompt, /export map stale/) assert.match(prompt, /## How to reproduce/) assert.match(prompt, /## Preliminary investigation/) assert.match(prompt, /`src\/index\.ts`/) }) })