Signed-off-by: Max <ai-agent@topdoglabs.com>
This commit is contained in:
parent
f77932435e
commit
7687dfd8a6
@ -7,7 +7,8 @@
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint",
|
||||
"analyze": "ANALYZE=true npm run build"
|
||||
"analyze": "ANALYZE=true npm run build",
|
||||
"test:refactor": "tsx --test scripts/tests/sprintSelection.test.ts && bash scripts/tests/refactor-cli-api.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
|
||||
290
scripts/tests/refactor-cli-api.sh
Executable file
290
scripts/tests/refactor-cli-api.sh
Executable file
@ -0,0 +1,290 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
MOCK_BIN="$TMP_DIR/mockbin"
|
||||
MOCK_LOG="$TMP_DIR/mock-curl.log"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$TMP_DIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$MOCK_BIN"
|
||||
touch "$MOCK_LOG"
|
||||
|
||||
cat > "$MOCK_BIN/curl" <<'MOCK_CURL'
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
method="GET"
|
||||
url=""
|
||||
data=""
|
||||
cookie_out=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-X)
|
||||
method="$2"
|
||||
shift 2
|
||||
;;
|
||||
--data|-d)
|
||||
data="$2"
|
||||
shift 2
|
||||
;;
|
||||
-c)
|
||||
cookie_out="$2"
|
||||
shift 2
|
||||
;;
|
||||
-b|-H|-w)
|
||||
shift 2
|
||||
;;
|
||||
-s|-sS)
|
||||
shift
|
||||
;;
|
||||
http://*|https://*)
|
||||
url="$1"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -n "$cookie_out" ]]; then
|
||||
mkdir -p "$(dirname "$cookie_out")"
|
||||
touch "$cookie_out"
|
||||
fi
|
||||
|
||||
echo "${method} ${url} ${data}" >> "${MOCK_CURL_LOG:?}"
|
||||
|
||||
respond() {
|
||||
printf '%s\n%s\n' "$1" "$2"
|
||||
}
|
||||
|
||||
case "${method} ${url}" in
|
||||
"GET http://localhost:3000/api/projects")
|
||||
respond '{"projects":[{"id":"p1","name":"Proj","description":"Demo","color":"#3b82f6"}]}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/projects/p1")
|
||||
respond '{"project":{"id":"p1","name":"Proj","description":"Demo","color":"#3b82f6"}}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/projects")
|
||||
respond '{"success":true,"project":{"id":"p2","name":"New Proj"}}' 200
|
||||
;;
|
||||
"PATCH http://localhost:3000/api/projects")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"DELETE http://localhost:3000/api/projects")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/auth/users")
|
||||
respond '{"users":[{"id":"u1","name":"Max","email":"max@example.com"}]}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/auth/login")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/auth/logout")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/auth/session")
|
||||
respond '{"authenticated":true}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/auth/register")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/auth/forgot-password")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/auth/reset-password")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"PATCH http://localhost:3000/api/auth/account")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/sprints"|\
|
||||
"GET http://localhost:3000/api/sprints?status=active")
|
||||
respond '{"sprints":[{"id":"s1","name":"Sprint 1","projectId":"p1","status":"active","startDate":"2026-02-20","endDate":"2026-03-01"}]}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/sprints/s1")
|
||||
respond '{"sprint":{"id":"s1","name":"Sprint 1","projectId":"p1","status":"active","startDate":"2026-02-20","endDate":"2026-03-01"}}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/sprints")
|
||||
respond '{"success":true,"sprint":{"id":"s2","name":"Sprint 2","projectId":"p1"}}' 200
|
||||
;;
|
||||
"PATCH http://localhost:3000/api/sprints")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"DELETE http://localhost:3000/api/sprints")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/sprints/current")
|
||||
respond '{"sprint":{"id":"s1","name":"Sprint 1"}}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/sprints/current?projectId=p1")
|
||||
respond '{"sprint":{"id":"s1","name":"Sprint 1","projectId":"p1"}}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/sprints/close")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/tasks"|\
|
||||
"GET http://localhost:3000/api/tasks?scope=all")
|
||||
respond '{"tasks":[{"id":"t1","title":"Demo Task","status":"open","priority":"medium","type":"task","projectId":"p1","assigneeId":"u1","sprintId":"s1","comments":[],"tags":[],"attachments":[]}]}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/tasks?taskId=t1&include=detail")
|
||||
respond '{"tasks":[{"id":"t1","title":"Demo Task","status":"open","priority":"medium","type":"task","projectId":"p1","assigneeId":"u1","sprintId":"s1","comments":[],"tags":[],"attachments":[]}]}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/tasks")
|
||||
respond '{"success":true,"task":{"id":"t1"}}' 200
|
||||
;;
|
||||
"DELETE http://localhost:3000/api/tasks")
|
||||
respond '{"success":true}' 200
|
||||
;;
|
||||
"POST http://localhost:3000/api/tasks/natural")
|
||||
respond '{"success":true,"task":{"id":"t2","title":"Natural"}}' 200
|
||||
;;
|
||||
"GET http://localhost:3000/api/debug")
|
||||
respond '{"ok":true}' 200
|
||||
;;
|
||||
*)
|
||||
respond "{\"error\":\"Unhandled mock request: ${method} ${url}\"}" 500
|
||||
;;
|
||||
esac
|
||||
MOCK_CURL
|
||||
|
||||
chmod +x "$MOCK_BIN/curl"
|
||||
|
||||
assert_log_contains() {
|
||||
local expected="$1"
|
||||
if ! grep -F "$expected" "$MOCK_LOG" >/dev/null 2>&1; then
|
||||
echo "Expected mock curl log to contain: $expected" >&2
|
||||
echo "Actual log:" >&2
|
||||
cat "$MOCK_LOG" >&2
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
export HOME="$TMP_DIR/home"
|
||||
export PATH="$MOCK_BIN:$PATH"
|
||||
export MOCK_CURL_LOG="$MOCK_LOG"
|
||||
export API_URL="http://localhost:3000/api"
|
||||
|
||||
ATTACH_FILE="$TMP_DIR/attachment.txt"
|
||||
BULK_FILE="$TMP_DIR/tasks.json"
|
||||
cat > "$ATTACH_FILE" <<'ATTACH_EOF'
|
||||
refactor-test
|
||||
ATTACH_EOF
|
||||
cat > "$BULK_FILE" <<'BULK_EOF'
|
||||
[
|
||||
{
|
||||
"title": "Bulk task",
|
||||
"project": "Proj",
|
||||
"assignee": "Max",
|
||||
"sprint": "Sprint 1"
|
||||
}
|
||||
]
|
||||
BULK_EOF
|
||||
|
||||
"$ROOT_DIR/scripts/task.sh" list --json >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" get t1 >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" current-sprint >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" current-sprint --project Proj >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" create --title "API Task" --project "Proj" --assignee "Max" --sprint current --status todo --priority high >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" update t1 --status done --tags "api,refactor" --add-comment "done" >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" delete t1 >/dev/null
|
||||
"$ROOT_DIR/scripts/task.sh" bulk-create "$BULK_FILE" >/dev/null
|
||||
|
||||
"$ROOT_DIR/scripts/project.sh" list --json >/dev/null
|
||||
"$ROOT_DIR/scripts/project.sh" get "Proj" >/dev/null
|
||||
"$ROOT_DIR/scripts/project.sh" create --name "New Proj" --description "Desc" --color "#ffffff" >/dev/null
|
||||
"$ROOT_DIR/scripts/project.sh" update "Proj" --name "Renamed Proj" >/dev/null
|
||||
"$ROOT_DIR/scripts/project.sh" delete "Proj" >/dev/null
|
||||
|
||||
"$ROOT_DIR/scripts/sprint.sh" list --json >/dev/null
|
||||
"$ROOT_DIR/scripts/sprint.sh" list --active --json >/dev/null
|
||||
"$ROOT_DIR/scripts/sprint.sh" get "Sprint 1" >/dev/null
|
||||
"$ROOT_DIR/scripts/sprint.sh" create --name "Sprint 2" --project "Proj" --goal "Ship" --start-date "2026-02-24" --end-date "2026-03-03" >/dev/null
|
||||
"$ROOT_DIR/scripts/sprint.sh" update "Sprint 1" --status completed >/dev/null
|
||||
"$ROOT_DIR/scripts/sprint.sh" close "Sprint 1" >/dev/null
|
||||
"$ROOT_DIR/scripts/sprint.sh" delete "Sprint 1" >/dev/null
|
||||
|
||||
"$ROOT_DIR/scripts/gantt.sh" task list open >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task get t1 >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task create "From wrapper" todo high p1 >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task natural "Create natural task" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task update t1 status done >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task comment t1 "Looks good" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task attach t1 "$ATTACH_FILE" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" task delete t1 >/dev/null
|
||||
|
||||
"$ROOT_DIR/scripts/gantt.sh" project list >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" project get p1 >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" project create "Wrapper Project" "Desc" "#111111" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" project update p1 name "Renamed" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" project delete p1 >/dev/null
|
||||
|
||||
"$ROOT_DIR/scripts/gantt.sh" sprint list >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" sprint get s1 >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" sprint create "Wrapper Sprint" p1 "2026-02-24" "2026-03-03" "Goal" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" sprint update s1 status completed >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" sprint close s1 >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" sprint delete s1 >/dev/null
|
||||
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth login "max@example.com" "secret" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth logout >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth session >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth register "new@example.com" "secret" "New User" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth forgot-password "new@example.com" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth reset-password "tok123" "newsecret" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth account name "Renamed User" >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" auth users >/dev/null
|
||||
"$ROOT_DIR/scripts/gantt.sh" debug >/dev/null
|
||||
|
||||
if [[ ! -f "$HOME/.config/gantt-board/cookies.txt" ]]; then
|
||||
echo "Expected cookie file to be created at $HOME/.config/gantt-board/cookies.txt" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
assert_log_contains "GET http://localhost:3000/api/projects"
|
||||
assert_log_contains "POST http://localhost:3000/api/projects"
|
||||
assert_log_contains "PATCH http://localhost:3000/api/projects"
|
||||
assert_log_contains "DELETE http://localhost:3000/api/projects"
|
||||
assert_log_contains "GET http://localhost:3000/api/projects/p1"
|
||||
|
||||
assert_log_contains "GET http://localhost:3000/api/sprints"
|
||||
assert_log_contains "GET http://localhost:3000/api/sprints?status=active"
|
||||
assert_log_contains "GET http://localhost:3000/api/sprints/s1"
|
||||
assert_log_contains "GET http://localhost:3000/api/sprints/current"
|
||||
assert_log_contains "GET http://localhost:3000/api/sprints/current?projectId=p1"
|
||||
assert_log_contains "POST http://localhost:3000/api/sprints"
|
||||
assert_log_contains "PATCH http://localhost:3000/api/sprints"
|
||||
assert_log_contains "POST http://localhost:3000/api/sprints/close"
|
||||
assert_log_contains "DELETE http://localhost:3000/api/sprints"
|
||||
|
||||
assert_log_contains "GET http://localhost:3000/api/tasks"
|
||||
assert_log_contains "GET http://localhost:3000/api/tasks?scope=all"
|
||||
assert_log_contains "GET http://localhost:3000/api/tasks?taskId=t1&include=detail"
|
||||
assert_log_contains "POST http://localhost:3000/api/tasks"
|
||||
assert_log_contains "DELETE http://localhost:3000/api/tasks"
|
||||
assert_log_contains "POST http://localhost:3000/api/tasks/natural"
|
||||
|
||||
assert_log_contains "POST http://localhost:3000/api/auth/login"
|
||||
assert_log_contains "POST http://localhost:3000/api/auth/logout"
|
||||
assert_log_contains "GET http://localhost:3000/api/auth/session"
|
||||
assert_log_contains "POST http://localhost:3000/api/auth/register"
|
||||
assert_log_contains "POST http://localhost:3000/api/auth/forgot-password"
|
||||
assert_log_contains "POST http://localhost:3000/api/auth/reset-password"
|
||||
assert_log_contains "PATCH http://localhost:3000/api/auth/account"
|
||||
assert_log_contains "GET http://localhost:3000/api/auth/users"
|
||||
assert_log_contains "GET http://localhost:3000/api/debug"
|
||||
|
||||
if rg -n --hidden -S "rest/v1|SUPABASE_URL|SERVICE_KEY|ANON_KEY|qnatchrjlpehiijwtreh" "$ROOT_DIR/scripts" --glob "!scripts/tests/*" >/dev/null 2>&1; then
|
||||
echo "Direct Supabase references were found in scripts/" >&2
|
||||
rg -n --hidden -S "rest/v1|SUPABASE_URL|SERVICE_KEY|ANON_KEY|qnatchrjlpehiijwtreh" "$ROOT_DIR/scripts" --glob "!scripts/tests/*" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "CLI API passthrough tests passed"
|
||||
63
scripts/tests/sprintSelection.test.ts
Normal file
63
scripts/tests/sprintSelection.test.ts
Normal file
@ -0,0 +1,63 @@
|
||||
import assert from "node:assert/strict"
|
||||
import test from "node:test"
|
||||
|
||||
import { findCurrentSprint, isSprintInProgress } from "../../src/lib/server/sprintSelection"
|
||||
|
||||
const NOW = new Date("2026-02-24T15:30:00")
|
||||
|
||||
test("isSprintInProgress uses inclusive boundaries", () => {
|
||||
assert.equal(isSprintInProgress("2026-02-24", "2026-02-24", NOW), true)
|
||||
assert.equal(isSprintInProgress("2026-02-25", "2026-02-26", NOW), false)
|
||||
})
|
||||
|
||||
test("findCurrentSprint prefers active sprint in range", () => {
|
||||
const sprint = findCurrentSprint(
|
||||
[
|
||||
{ id: "planning", status: "planning", startDate: "2026-02-20", endDate: "2026-02-28", projectId: "p1" },
|
||||
{ id: "active", status: "active", startDate: "2026-02-22", endDate: "2026-02-26", projectId: "p1" },
|
||||
],
|
||||
{ now: NOW, projectId: "p1" }
|
||||
)
|
||||
|
||||
assert.equal(sprint?.id, "active")
|
||||
})
|
||||
|
||||
test("findCurrentSprint falls back to non-completed in range", () => {
|
||||
const sprint = findCurrentSprint(
|
||||
[{ id: "planning", status: "planning", startDate: "2026-02-20", endDate: "2026-02-28", projectId: "p1" }],
|
||||
{ now: NOW, projectId: "p1" }
|
||||
)
|
||||
|
||||
assert.equal(sprint?.id, "planning")
|
||||
})
|
||||
|
||||
test("findCurrentSprint returns null for completed-only unless fallback enabled", () => {
|
||||
const sprints = [{ id: "done", status: "completed", startDate: "2026-02-20", endDate: "2026-02-28", projectId: "p1" }] as const
|
||||
|
||||
const withoutFallback = findCurrentSprint(sprints, { now: NOW, projectId: "p1" })
|
||||
const withFallback = findCurrentSprint(sprints, { now: NOW, projectId: "p1", includeCompletedFallback: true })
|
||||
|
||||
assert.equal(withoutFallback, null)
|
||||
assert.equal(withFallback?.id, "done")
|
||||
})
|
||||
|
||||
test("findCurrentSprint respects project scoping", () => {
|
||||
const sprint = findCurrentSprint(
|
||||
[
|
||||
{ id: "p1-active", status: "active", startDate: "2026-02-20", endDate: "2026-02-28", projectId: "p1" },
|
||||
{ id: "p2-active", status: "active", startDate: "2026-02-20", endDate: "2026-02-28", projectId: "p2" },
|
||||
],
|
||||
{ now: NOW, projectId: "p2" }
|
||||
)
|
||||
|
||||
assert.equal(sprint?.id, "p2-active")
|
||||
})
|
||||
|
||||
test("findCurrentSprint returns null when no sprint is in range", () => {
|
||||
const sprint = findCurrentSprint(
|
||||
[{ id: "future", status: "active", startDate: "2026-03-01", endDate: "2026-03-07", projectId: "p1" }],
|
||||
{ now: NOW, projectId: "p1" }
|
||||
)
|
||||
|
||||
assert.equal(sprint, null)
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user