Skip to content
← Public packages

@kentcdodds/friction-log

Qualify and file friction papercuts to GitHub (any owner/repo) or wake Patch for Kody packages; daily sweep of kody-hq/kody friction issues.

FollowFork

src/qualify.test.ts

163 lines · 5.5 KB · TypeScript
Raw
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`/)
	})
})