← 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.
src/qualify.test.ts
163 lines · 5.5 KB · TypeScriptimport 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`/)
})
})