chore: remove scripts
migrated to cli submodule
This commit is contained in:
parent
e6901e4f6e
commit
3f0152bd2b
16 changed files with 0 additions and 2825 deletions
|
|
@ -1 +0,0 @@
|
||||||
registry=http://registry.npmjs.org/
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
#!/usr/bin/env node
|
|
||||||
// mime-todo/bin/main.ts
|
|
||||||
import { parseTodoFile } from "../lib/file"
|
|
||||||
|
|
||||||
|
|
||||||
async function main() {
|
|
||||||
const [, , cmd, ...args] = process.argv
|
|
||||||
const todo = await parseTodoFile()
|
|
||||||
|
|
||||||
switch (cmd) {
|
|
||||||
case "issues":
|
|
||||||
for (const issue of todo.issues) {
|
|
||||||
console.log(`#${issue.id} [${issue.type}] (${issue.status}) ${issue.title}`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case "sprints":
|
|
||||||
for (const sprint of todo.sprints) {
|
|
||||||
console.log(`${sprint.name}: ${sprint.start}..${sprint.end}`)
|
|
||||||
}
|
|
||||||
break
|
|
||||||
|
|
||||||
case "issues-in-sprint": {
|
|
||||||
const name = args.join(" ")
|
|
||||||
const sprint = todo.sprints.find(s => s.name === name)
|
|
||||||
if (!sprint) {
|
|
||||||
console.error(`Sprint not found: ${name}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
const { start, end } = sprint
|
|
||||||
for (const issue of todo.issues) {
|
|
||||||
const ds = issue.dueStart ?? issue.dueEnd
|
|
||||||
const de = issue.dueEnd ?? issue.dueStart
|
|
||||||
if (!ds || !de) continue
|
|
||||||
if (ds <= end && de >= start) {
|
|
||||||
console.log(`#${issue.id} [${issue.type}] (${issue.status}) ${issue.title}`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.log(`Usage:
|
|
||||||
todo list
|
|
||||||
todo sprints
|
|
||||||
todo issues-in-sprint <name>`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
main().catch(err => {
|
|
||||||
console.error(err)
|
|
||||||
process.exit(1)
|
|
||||||
})
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
{
|
|
||||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
|
||||||
"title": "TODO File Schema",
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"sprints": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "$ref": "#/definitions/Sprint" }
|
|
||||||
},
|
|
||||||
"issues": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "$ref": "#/definitions/Issue" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["sprints", "issues"],
|
|
||||||
"additionalProperties": false,
|
|
||||||
|
|
||||||
"definitions": {
|
|
||||||
"Sprint": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"name": { "type": "string" },
|
|
||||||
"start": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
||||||
},
|
|
||||||
"end": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": ["name", "start", "end"],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"Issue": {
|
|
||||||
"type": "object",
|
|
||||||
"properties": {
|
|
||||||
"id": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1
|
|
||||||
},
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["feature", "bugfix", "hotfix"]
|
|
||||||
},
|
|
||||||
"title": { "type": "string" },
|
|
||||||
"status": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["open", "in-progress", "done", "hold", "cancelled"]
|
|
||||||
},
|
|
||||||
"priority": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["low", "medium", "high"]
|
|
||||||
},
|
|
||||||
"created": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
||||||
},
|
|
||||||
"relationships": {
|
|
||||||
"$ref": "#/definitions/IssueRelationships"
|
|
||||||
},
|
|
||||||
"dueStart": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
||||||
},
|
|
||||||
"dueEnd": {
|
|
||||||
"type": "string",
|
|
||||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
|
||||||
},
|
|
||||||
"description": { "type": "string" },
|
|
||||||
"body": { "type": "string" }
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"id",
|
|
||||||
"type",
|
|
||||||
"title",
|
|
||||||
"status",
|
|
||||||
"priority",
|
|
||||||
"created",
|
|
||||||
"relationships",
|
|
||||||
"description",
|
|
||||||
"body"
|
|
||||||
],
|
|
||||||
"additionalProperties": false
|
|
||||||
},
|
|
||||||
|
|
||||||
"IssueRelationships": {
|
|
||||||
"type": "object",
|
|
||||||
"patternProperties": {
|
|
||||||
"^(dependsOn|relatesTo|blocks)$": {
|
|
||||||
"type": "array",
|
|
||||||
"items": { "type": "integer", "minimum": 1 }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"additionalProperties": false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
// mime-todo/lib/file.ts
|
|
||||||
import * as fs from "fs"
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
|
|
||||||
import { simpleParser } from "mailparser"
|
|
||||||
import yargs from 'yargs';
|
|
||||||
import { hideBin } from 'yargs/helpers';
|
|
||||||
|
|
||||||
import { parseIssue, Issue } from "./issue"
|
|
||||||
import { parseSprints, Sprint } from "./sprint"
|
|
||||||
|
|
||||||
import * as schema from "./file.schema.json"
|
|
||||||
|
|
||||||
import Ajv from "ajv"
|
|
||||||
|
|
||||||
const ajv = new Ajv({ allErrors: true })
|
|
||||||
|
|
||||||
const validateFile = ajv.compile(schema)
|
|
||||||
|
|
||||||
export interface TodoFile {
|
|
||||||
sprints: Sprint[]
|
|
||||||
issues: Issue[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseMime(mimeText: string) {
|
|
||||||
return await simpleParser(mimeText)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function preprocessTODO(raw: string): string {
|
|
||||||
const boundary = "ISSUE"
|
|
||||||
|
|
||||||
const rawParts = raw
|
|
||||||
.split(`--${boundary}`)
|
|
||||||
.map(p => p.trim())
|
|
||||||
.filter(Boolean)
|
|
||||||
|
|
||||||
interface Part {
|
|
||||||
type: string
|
|
||||||
body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts: Part[] = []
|
|
||||||
|
|
||||||
for (const rawPart of rawParts) {
|
|
||||||
const lines = rawPart.split(/\r?\n/)
|
|
||||||
const typeLine = lines[0]
|
|
||||||
|
|
||||||
if (!typeLine?.toLowerCase().startsWith("content-type:")) {
|
|
||||||
throw new Error(`Part missing Content-Type header:\n${rawPart}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = typeLine.slice("Content-Type:".length).trim()
|
|
||||||
const body = lines.slice(1).join("\n").trim()
|
|
||||||
|
|
||||||
parts.push({ type, body })
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate sprint parts
|
|
||||||
const sprintParts = parts.filter(p => p.type === "application/sprints")
|
|
||||||
if (sprintParts.length > 1) {
|
|
||||||
throw new Error("Multiple application/sprints parts found")
|
|
||||||
}
|
|
||||||
|
|
||||||
const sprintsPart = sprintParts[0] ?? null
|
|
||||||
|
|
||||||
// Preserve unknown types, but reorder sprints first
|
|
||||||
const orderedParts: Part[] = []
|
|
||||||
if (sprintsPart) orderedParts.push(sprintsPart)
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
if (part !== sprintsPart) orderedParts.push(part)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MIME envelope
|
|
||||||
const out: string[] = []
|
|
||||||
out.push(`MIME-Version: 1.0`)
|
|
||||||
out.push(`Content-Type: multipart/mixed; boundary="${boundary}"`)
|
|
||||||
out.push("")
|
|
||||||
|
|
||||||
for (const part of orderedParts) {
|
|
||||||
out.push(`--${boundary}`)
|
|
||||||
out.push(`Content-Type: ${part.type}`)
|
|
||||||
out.push("")
|
|
||||||
out.push(part.body)
|
|
||||||
out.push("")
|
|
||||||
}
|
|
||||||
|
|
||||||
out.push(`--${boundary}--`)
|
|
||||||
out.push("")
|
|
||||||
|
|
||||||
return out.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function parseTodoFile(path = "TODO"): Promise<TodoFile> {
|
|
||||||
const raw = fs.readFileSync(path, "utf-8")
|
|
||||||
const mimeWrapped = preprocessTODO(raw)
|
|
||||||
const parsed = await parseMime(mimeWrapped)
|
|
||||||
|
|
||||||
const sprints: Sprint[] = []
|
|
||||||
const issues: Issue[] = []
|
|
||||||
|
|
||||||
const parts =
|
|
||||||
(parsed as any).attachments ??
|
|
||||||
[]
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
|
|
||||||
const contentType = String(part.contentType || "").toLowerCase()
|
|
||||||
const body = part.content?.toString("utf-8") ?? ""
|
|
||||||
|
|
||||||
if (contentType.startsWith("application/sprints")) {
|
|
||||||
// Debug: Log what we're parsing
|
|
||||||
// console.log("Parsing sprints:", body);
|
|
||||||
sprints.push(...parseSprints(body))
|
|
||||||
} else if (contentType.startsWith("application/issue")) {
|
|
||||||
issues.push(parseIssue(body))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const file = {sprints: sprints, issues: issues };
|
|
||||||
|
|
||||||
if (!validateFile(file)) {
|
|
||||||
throw new Error(
|
|
||||||
"Sprint schema validation failed: " +
|
|
||||||
JSON.stringify(validateFile.errors, null, 2)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function index(yargs) {
|
|
||||||
console.log(yargs.argv);
|
|
||||||
}
|
|
||||||
|
|
||||||
function _index(yargs) {
|
|
||||||
return yargs.command('index', 'welcome ter yargs!', index)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
||||||
|
|
||||||
let argv = yargs()
|
|
||||||
.usage('$0 <cmd> [args]')
|
|
||||||
.help()
|
|
||||||
|
|
||||||
let commands = [
|
|
||||||
_index
|
|
||||||
]
|
|
||||||
|
|
||||||
commands.forEach((method) => {
|
|
||||||
method(argv)
|
|
||||||
});
|
|
||||||
|
|
||||||
yargs.parse(hideBin(process.argv))
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +0,0 @@
|
||||||
// mime-todo/lib/issue.ts
|
|
||||||
|
|
||||||
|
|
||||||
export type RelationshipKind = "dependsOn" | "relatesTo" | "blocks"
|
|
||||||
|
|
||||||
export interface IssueRelationships {
|
|
||||||
[kind: string]: number[]
|
|
||||||
}
|
|
||||||
|
|
||||||
export type IssueType = "feature" | "bugfix" | "hotfix"
|
|
||||||
export type IssueStatus = "open" | "in-progress" | "done" | "hold" | "cancelled"
|
|
||||||
export type IssuePriority = "low" | "medium" | "high"
|
|
||||||
|
|
||||||
export interface Issue {
|
|
||||||
id: number
|
|
||||||
type: IssueType
|
|
||||||
title: string
|
|
||||||
status: IssueStatus
|
|
||||||
priority: IssuePriority
|
|
||||||
created: string // YYYY-MM-DD
|
|
||||||
relationships: IssueRelationships
|
|
||||||
dueStart?: string // YYYY-MM-DD
|
|
||||||
dueEnd?: string // YYYY-MM-DD
|
|
||||||
description: string
|
|
||||||
body: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseRelationships(text: string): IssueRelationships {
|
|
||||||
const relationships: IssueRelationships = {}
|
|
||||||
|
|
||||||
// Handle empty relationships
|
|
||||||
if (!text || text.trim() === "") {
|
|
||||||
return relationships
|
|
||||||
}
|
|
||||||
|
|
||||||
// Simple parsing for format like "dependsOn:3"
|
|
||||||
const parts = text.split(/,\s*/).filter(p => p.trim() !== "")
|
|
||||||
for (const part of parts) {
|
|
||||||
const [key, value] = part.split(":").map(s => s.trim())
|
|
||||||
if (key && value) {
|
|
||||||
relationships[key] = value.split(/[\s]+/).map(Number).filter(n => !isNaN(n))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return relationships
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseIssue(text: string): Issue {
|
|
||||||
const lines = text.split(/\r?\n/)
|
|
||||||
const issue: Partial<Issue> = {}
|
|
||||||
|
|
||||||
let inDescription = false
|
|
||||||
const descLines: string[] = []
|
|
||||||
const bodyLines: string[] = []
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (!inDescription) {
|
|
||||||
if (line.startsWith("ID:")) issue.id = Number(line.slice(3).trim())
|
|
||||||
else if (line.startsWith("Type:")) issue.type = line.slice(5).trim()
|
|
||||||
else if (line.startsWith("Title:")) issue.title = line.slice(6).trim()
|
|
||||||
else if (line.startsWith("Status:")) issue.status = line.slice(7).trim()
|
|
||||||
else if (line.startsWith("Priority:")) issue.priority = line.slice(9).trim()
|
|
||||||
else if (line.startsWith("Created:")) issue.created = line.slice(8).trim()
|
|
||||||
else if (line.startsWith("DueStart:")) issue.dueStart = line.slice(9).trim()
|
|
||||||
else if (line.startsWith("DueEnd:")) issue.dueEnd = line.slice(7).trim()
|
|
||||||
else if (line.startsWith("Relationships:")) {
|
|
||||||
const rel = line.slice("Relationships:".length).trim()
|
|
||||||
issue.relationships = parseRelationships(rel)
|
|
||||||
}
|
|
||||||
else if (line.startsWith("Description:")) {
|
|
||||||
inDescription = true
|
|
||||||
descLines.push(line.slice("Description:".length).trim())
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Description continuation
|
|
||||||
if (line.trim() === "") {
|
|
||||||
// blank line ends description
|
|
||||||
inDescription = false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if (/^\s+/.test(line)) {
|
|
||||||
descLines.push(line.trim())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// first non-indented line ends description
|
|
||||||
inDescription = false
|
|
||||||
bodyLines.push(line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
issue.description = descLines.join("\n")
|
|
||||||
issue.body = bodyLines.join("\n")
|
|
||||||
|
|
||||||
return issue as Issue
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
//**/*
|
|
||||||
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
// mime-todo/lib/sprint.ts
|
|
||||||
export interface Sprint {
|
|
||||||
name: string
|
|
||||||
start: string // YYYY-MM-DD
|
|
||||||
end: string // YYYY-MM-DD
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseSprints(text: string): Sprint[] {
|
|
||||||
const lines = text.split(/\r?\n/)
|
|
||||||
const sprints: Sprint[] = []
|
|
||||||
|
|
||||||
let current: Partial<Sprint> | null = null
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
// Start of sprint entry (must not trim indentation)
|
|
||||||
if (/^\s*-\s*(Name:.*)?$/.test(line)) {
|
|
||||||
if (current) sprints.push(current as Sprint)
|
|
||||||
current = {}
|
|
||||||
|
|
||||||
const match = line.match(/^\s*-\s*Name:\s*(.*)$/)
|
|
||||||
if (match) current.name = match[1]
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Key-value pairs (must be indented)
|
|
||||||
const kv = line.match(/^\s+([A-Za-z][A-Za-z0-9]*):\s*(.*)$/)
|
|
||||||
if (kv && current) {
|
|
||||||
const key = kv[1]
|
|
||||||
const value = kv[2]
|
|
||||||
|
|
||||||
if (key === "Name") current.name = value
|
|
||||||
if (key === "Range") {
|
|
||||||
const [start, end] = value.split("..")
|
|
||||||
current.start = start
|
|
||||||
current.end = end
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (current) sprints.push(current as Sprint)
|
|
||||||
return sprints
|
|
||||||
}
|
|
||||||
2172
scripts/ts-mime-todo/package-lock.json
generated
2172
scripts/ts-mime-todo/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,26 +0,0 @@
|
||||||
{
|
|
||||||
"name": "mime-todo",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
|
||||||
"@types/yargs": "^17.0.35",
|
|
||||||
"ajv": "^8.17.1",
|
|
||||||
"mailparser": "^3.9.3",
|
|
||||||
"tsx": "^3.7.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"vitest": "^4.0.18"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"issues": "tsx scripts/todo/main.ts issues",
|
|
||||||
"sprints": "tsx scripts/todo/main.ts sprints",
|
|
||||||
"issues-in-sprint": "tsx scripts/todo/main.ts issues-in-sprint"
|
|
||||||
},
|
|
||||||
"main": "dist/main.js",
|
|
||||||
"directories": {
|
|
||||||
"lib": "lib",
|
|
||||||
"test": "tests"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/sprints
|
|
||||||
Sprints:
|
|
||||||
- Name: Sprint Alpha
|
|
||||||
Range: 2026-02-01..2026-02-14
|
|
||||||
-
|
|
||||||
Name: Sprint Beta
|
|
||||||
Range: 2026-02-15..2026-02-28
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
ID: 1
|
|
||||||
Type: feature
|
|
||||||
Title: Add streaming parser
|
|
||||||
Status: open
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-02-05
|
|
||||||
Relationships: dependsOn:3
|
|
||||||
Description: Implement streaming JSON parser.
|
|
||||||
Must support SAX-like events.
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
ID: 2
|
|
||||||
Type: bugfix
|
|
||||||
Title: Fix wraparound
|
|
||||||
Status: in-progress
|
|
||||||
Priority: medium
|
|
||||||
Created: 2026-02-06
|
|
||||||
Relationships:
|
|
||||||
Description: Fix off-by-one in circular buffer.
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
ID: 10
|
|
||||||
Type: hotfix
|
|
||||||
Title: Patch crash
|
|
||||||
Status: done
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-02-03
|
|
||||||
Relationships:
|
|
||||||
Description: Fix crash in allocator.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/sprints
|
|
||||||
Sprints:
|
|
||||||
- Name: Sprint Alpha
|
|
||||||
Range: 2026-02-01..2026-02-14
|
|
||||||
-
|
|
||||||
Name: Sprint Beta
|
|
||||||
Range: 2026-02-15..2026-02-28
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/sprints
|
|
||||||
Sprints:
|
|
||||||
- Name: Sprint Gamma
|
|
||||||
Range: 2026-02-28..2026-03-15
|
|
||||||
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/issue
|
|
||||||
ID: 1
|
|
||||||
Type: feature
|
|
||||||
Title: Add streaming parser
|
|
||||||
Status: open
|
|
||||||
Priority: high
|
|
||||||
Created: 2026-02-05
|
|
||||||
Relationships: dependsOn:3
|
|
||||||
Description: Implement streaming JSON parser.
|
|
||||||
Must support SAX-like events.
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
Sprints:
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
import * as fs from "fs"
|
|
||||||
import { describe, it, expect } from "vitest"
|
|
||||||
import { parseSprints, preprocessTODO, parseTodoFile } from "../../lib/file"
|
|
||||||
|
|
||||||
|
|
||||||
describe("parseTodoFile", () => {
|
|
||||||
it("parses full TODO file end-to-end", async () => {
|
|
||||||
const todo = await parseTodoFile("tests/_mocks/todo-basic.txt")
|
|
||||||
|
|
||||||
expect(todo.sprints.length).toBe(2)
|
|
||||||
|
|
||||||
expect(todo.issues.length).toBe(2)
|
|
||||||
|
|
||||||
// expect(todo.issues[0].title).toBe("Add streaming parser")
|
|
||||||
expect(todo.sprints[0].name).toBe("Sprint Alpha")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("works with TODO containing only issues", async () => {
|
|
||||||
const todo = await parseTodoFile("tests/_mocks/todo-issues-only.txt")
|
|
||||||
expect(todo.sprints.length).toBe(0)
|
|
||||||
// expect(todo.issues.length).toBe(1) - skipping for now
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
describe("preprocessTODO", () => {
|
|
||||||
it("wraps TODO into MIME and puts sprints first", () => {
|
|
||||||
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
|
||||||
const mime = preprocessTODO(raw)
|
|
||||||
|
|
||||||
expect(mime).toContain("MIME-Version: 1.0")
|
|
||||||
expect(mime).toContain('Content-Type: multipart/mixed; boundary="ISSUE"')
|
|
||||||
|
|
||||||
const firstPartIndex = mime.indexOf("Content-Type: application/sprints")
|
|
||||||
const secondPartIndex = mime.indexOf("Content-Type: application/issue")
|
|
||||||
|
|
||||||
expect(firstPartIndex).toBeLessThan(secondPartIndex)
|
|
||||||
})
|
|
||||||
|
|
||||||
it("throws on multiple sprints parts", () => {
|
|
||||||
const raw = fs.readFileSync("tests/_mocks/todo-multiple-sprints.txt", "utf-8")
|
|
||||||
expect(() => preprocessTODO(raw)).toThrow()
|
|
||||||
})
|
|
||||||
|
|
||||||
it("preserves unknown MIME types", () => {
|
|
||||||
const raw = `
|
|
||||||
--ISSUE
|
|
||||||
Content-Type: application/unknown
|
|
||||||
|
|
||||||
Hello world
|
|
||||||
`
|
|
||||||
const mime = preprocessTODO(raw)
|
|
||||||
expect(mime).toContain("application/unknown")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
import { describe, it, expect } from "vitest"
|
|
||||||
import * as fs from "fs"
|
|
||||||
import { parseIssue } from "../../lib/issue"
|
|
||||||
|
|
||||||
describe("parseIssue", () => {
|
|
||||||
it("parses all required fields", () => {
|
|
||||||
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
|
||||||
const issueText = raw.split("Content-Type: application/issue")[1]
|
|
||||||
const issue = parseIssue(issueText)
|
|
||||||
|
|
||||||
expect(issue.id).toBe(1)
|
|
||||||
expect(issue.type).toBe("feature")
|
|
||||||
expect(issue.status).toBe("open")
|
|
||||||
expect(issue.priority).toBe("high")
|
|
||||||
expect(issue.description).toContain("Implement streaming JSON parser.")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("parses empty relationships", () => {
|
|
||||||
const raw = `
|
|
||||||
ID: 2
|
|
||||||
Type: bugfix
|
|
||||||
Title: T
|
|
||||||
Status: open
|
|
||||||
Priority: low
|
|
||||||
Created: 2026-02-01
|
|
||||||
Relationships:
|
|
||||||
Description: X
|
|
||||||
`
|
|
||||||
const issue = parseIssue(raw)
|
|
||||||
expect(issue.relationships).toEqual({})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
import * as fs from "fs"
|
|
||||||
import { describe, it, expect } from "vitest"
|
|
||||||
import { parseSprints } from "../../lib/sprint"
|
|
||||||
|
|
||||||
describe("parseSprints", () => {
|
|
||||||
it("parses compact and expanded sprint entries", () => {
|
|
||||||
const raw = fs.readFileSync("tests/_mocks/todo-basic.txt", "utf-8")
|
|
||||||
const sprintsText = raw.split("Content-Type: application/sprints")[1]
|
|
||||||
const sprints = parseSprints(sprintsText)
|
|
||||||
|
|
||||||
expect(sprints.length).toBe(2)
|
|
||||||
expect(sprints[0].name).toBe("Sprint Alpha")
|
|
||||||
expect(sprints[0].start).toBe("2026-02-01")
|
|
||||||
expect(sprints[0].end).toBe("2026-02-14")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("handles TODO with no sprints", () => {
|
|
||||||
const raw = fs.readFileSync("tests/_mocks/todo-no-sprints.txt", "utf-8")
|
|
||||||
const sprints = parseSprints(raw)
|
|
||||||
expect(sprints.length).toBe(0)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue