feat: Seeded acceptance-ready job data through the live API with determ…
- "scripts/s06-acceptance-data.sh" - "scripts/s06-acceptance-data.test.sh" - "README.md" - ".gsd/KNOWLEDGE.md" - ".gsd/DECISIONS.md" - ".gsd/milestones/M001/slices/S06/tasks/T02-SUMMARY.md" GSD-Task: S06/T02
This commit is contained in:
Executable
+294
@@ -0,0 +1,294 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Seed one deterministic acceptance-ready job into the live API.
|
||||
#
|
||||
# This script requires AUTH_TOKEN because local dev auth is enabled in this milestone.
|
||||
# Retrieve a bearer token manually before running, for example by logging into the local
|
||||
# API via POST /api/auth/login with the active local account for this environment, then:
|
||||
# export AUTH_TOKEN="<bearer token>"
|
||||
# bash scripts/s06-acceptance-data.sh
|
||||
#
|
||||
# If the placeholder dev credentials in appsettings.Development.json do not match the
|
||||
# current database, use the real seeded account for this environment or a bearer token
|
||||
# captured from an already-authenticated local browser session. The script never prints
|
||||
# token values.
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
API_BASE="${API_BASE:-http://localhost:5202/api}"
|
||||
AUTH_TOKEN="${AUTH_TOKEN:-}"
|
||||
CURL_TIMEOUT="${CURL_TIMEOUT:-20}"
|
||||
CONNECT_TIMEOUT="${CONNECT_TIMEOUT:-5}"
|
||||
COMPANY_NAME="S06 Acceptance Labs"
|
||||
JOB_TITLE="S06 Acceptance Backend Engineer"
|
||||
JOB_URL="https://example.invalid/jobs/s06-acceptance-backend-engineer"
|
||||
THREAD_ID="s06-acceptance-thread"
|
||||
MESSAGE_ID="s06-acceptance-message-1"
|
||||
FOLLOW_UP_AT="2026-03-10T09:00:00Z"
|
||||
DATE_APPLIED="2026-02-28T10:00:00Z"
|
||||
CORRESPONDENCE_AT="2026-03-09T11:30:00Z"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
note() {
|
||||
printf '%s\n' "$*"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf 'ERROR: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if [[ -z "$AUTH_TOKEN" ]]; then
|
||||
fail "AUTH_TOKEN is required. Export a bearer token first, then rerun."
|
||||
fi
|
||||
|
||||
validate_json() {
|
||||
local file="$1"
|
||||
if ! python3 - "$file" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
with open(sys.argv[1], 'r', encoding='utf-8') as fh:
|
||||
json.load(fh)
|
||||
PY
|
||||
then
|
||||
printf 'Raw response body:\n' >&2
|
||||
cat "$file" >&2
|
||||
printf '\n' >&2
|
||||
fail "Malformed JSON returned by API."
|
||||
fi
|
||||
}
|
||||
|
||||
request() {
|
||||
local method="$1"
|
||||
local name="$2"
|
||||
local url="$3"
|
||||
local body_file="$4"
|
||||
local expect_json="$5"
|
||||
local data_file="${6:-}"
|
||||
local err_file="$TMP_DIR/${name// /-}.curl.err"
|
||||
local status_file="$TMP_DIR/${name// /-}.status"
|
||||
: >"$err_file"
|
||||
|
||||
local -a curl_args=(
|
||||
-sS
|
||||
--connect-timeout "$CONNECT_TIMEOUT"
|
||||
--max-time "$CURL_TIMEOUT"
|
||||
-X "$method"
|
||||
-H 'Accept: application/json'
|
||||
-H "Authorization: Bearer ${AUTH_TOKEN}"
|
||||
-o "$body_file"
|
||||
-w '%{http_code}'
|
||||
)
|
||||
|
||||
if [[ -n "$data_file" ]]; then
|
||||
curl_args+=(-H 'Content-Type: application/json' --data-binary "@$data_file")
|
||||
fi
|
||||
|
||||
set +e
|
||||
curl "${curl_args[@]}" "$url" >"$status_file" 2>"$err_file"
|
||||
local curl_exit=$?
|
||||
set -e
|
||||
|
||||
local status='000'
|
||||
if [[ -s "$status_file" ]]; then
|
||||
status="$(tr -d '\r\n' <"$status_file")"
|
||||
fi
|
||||
|
||||
if [[ $curl_exit -ne 0 ]]; then
|
||||
case "$curl_exit:$status" in
|
||||
7:*|28:*|52:*|56:*|6:*)
|
||||
printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2
|
||||
fail "Cannot reach ${url}. Start the API and rerun."
|
||||
;;
|
||||
22:401|22:403)
|
||||
printf 'Response body:\n' >&2
|
||||
cat "$body_file" >&2 || true
|
||||
printf '\n' >&2
|
||||
fail "AUTH_TOKEN appears invalid or lacks access for ${name}."
|
||||
;;
|
||||
22:5*)
|
||||
printf 'Response body:\n' >&2
|
||||
cat "$body_file" >&2 || true
|
||||
printf '\n' >&2
|
||||
fail "${name} failed with server error HTTP ${status}."
|
||||
;;
|
||||
22:*)
|
||||
printf 'Response body:\n' >&2
|
||||
cat "$body_file" >&2 || true
|
||||
printf '\n' >&2
|
||||
fail "${name} failed with HTTP ${status}."
|
||||
;;
|
||||
*)
|
||||
printf 'curl: %s\n' "$(tr -d '\r' <"$err_file")" >&2
|
||||
fail "${name} failed with curl exit ${curl_exit}."
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if [[ "$status" == 401 || "$status" == 403 ]]; then
|
||||
printf 'Response body:\n' >&2
|
||||
cat "$body_file" >&2 || true
|
||||
printf '\n' >&2
|
||||
fail "AUTH_TOKEN appears invalid or lacks access for ${name}."
|
||||
fi
|
||||
|
||||
if [[ "$status" =~ ^5 ]]; then
|
||||
printf 'Response body:\n' >&2
|
||||
cat "$body_file" >&2 || true
|
||||
printf '\n' >&2
|
||||
fail "${name} failed with server error HTTP ${status}."
|
||||
fi
|
||||
|
||||
if [[ "$expect_json" == "json" ]]; then
|
||||
validate_json "$body_file"
|
||||
fi
|
||||
}
|
||||
|
||||
json_extract() {
|
||||
local file="$1"
|
||||
local expression="$2"
|
||||
python3 - "$file" "$expression" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
file_path, expression = sys.argv[1], sys.argv[2]
|
||||
with open(file_path, 'r', encoding='utf-8') as fh:
|
||||
data = json.load(fh)
|
||||
value = eval(expression, {'__builtins__': {'next': next, 'len': len}}, {'data': data})
|
||||
if value is None:
|
||||
print('')
|
||||
elif isinstance(value, bool):
|
||||
print('true' if value else 'false')
|
||||
elif isinstance(value, (dict, list)):
|
||||
print(json.dumps(value, separators=(',', ':')))
|
||||
else:
|
||||
print(value)
|
||||
PY
|
||||
}
|
||||
|
||||
json_string() {
|
||||
python3 - "$1" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
print(json.dumps(sys.argv[1]))
|
||||
PY
|
||||
}
|
||||
|
||||
write_json() {
|
||||
local path="$1"
|
||||
shift
|
||||
printf '%s\n' "$*" >"$path"
|
||||
}
|
||||
|
||||
urlencode() {
|
||||
python3 - "$1" <<'PY'
|
||||
import sys
|
||||
import urllib.parse
|
||||
print(urllib.parse.quote(sys.argv[1]))
|
||||
PY
|
||||
}
|
||||
|
||||
note "Running preflight before seeding acceptance data."
|
||||
bash "$SCRIPT_DIR/s06-preflight.sh" >/dev/null
|
||||
|
||||
company_body="$TMP_DIR/company.json"
|
||||
write_json "$company_body" "{\"name\":$(json_string "$COMPANY_NAME"),\"location\":$(json_string "Oslo, Norway"),\"source\":$(json_string "LinkedIn")}"
|
||||
company_response="$TMP_DIR/company-response.json"
|
||||
request POST "company create" "$API_BASE/companies" "$company_response" json "$company_body"
|
||||
company_id="$(json_extract "$company_response" 'data["id"]')"
|
||||
[[ -n "$company_id" ]] || fail "Company create response did not include an id."
|
||||
|
||||
company_update_body="$TMP_DIR/company-update.json"
|
||||
write_json "$company_update_body" "{\"name\":$(json_string "$COMPANY_NAME"),\"location\":$(json_string "Oslo, Norway"),\"source\":$(json_string "LinkedIn"),\"recruiterName\":$(json_string "Maria Recruiter"),\"recruiterEmail\":$(json_string "maria.recruiter@example.invalid"),\"recruiterLinkedIn\":$(json_string "https://www.linkedin.com/in/maria-recruiter"),\"lastContactedAt\":$(json_string "$CORRESPONDENCE_AT"),\"nextContactAt\":$(json_string "$FOLLOW_UP_AT"),\"pipelineStage\":$(json_string "Follow-up ready")}"
|
||||
company_update_response="$TMP_DIR/company-update-response.json"
|
||||
request PUT "company update" "$API_BASE/companies/$company_id" "$company_update_response" json "$company_update_body"
|
||||
|
||||
jobs_lookup="$TMP_DIR/jobs-lookup.json"
|
||||
encoded_title="$(urlencode "$JOB_TITLE")"
|
||||
request GET "job lookup" "$API_BASE/jobapplications?page=1&pageSize=15&q=$encoded_title" "$jobs_lookup" json
|
||||
job_id="$(json_extract "$jobs_lookup" 'next((item["id"] for item in data.get("items", []) if item.get("jobTitle") == "S06 Acceptance Backend Engineer" and ((item.get("company") or {}).get("name") == "S06 Acceptance Labs")), "")')"
|
||||
job_action="updated"
|
||||
|
||||
create_job_body="$TMP_DIR/create-job.json"
|
||||
write_json "$create_job_body" "{\"jobTitle\":$(json_string "$JOB_TITLE"),\"companyId\":$company_id,\"status\":$(json_string "Waiting"),\"location\":$(json_string "Oslo / Hybrid"),\"salary\":$(json_string "NOK 900000"),\"nextAction\":$(json_string "Review saved package and send a manual recruiter follow-up from the linked thread."),\"followUpAt\":$(json_string "$FOLLOW_UP_AT"),\"notes\":$(json_string $'Acceptance seed notes for S06.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved acceptance application answer with concrete API and workflow evidence.\n<<<END_APPLICATION_ANSWER_DRAFT>>>'),\"description\":$(json_string "Lead the backend of an individual-first job tracking platform, maintain recruiter-thread continuity, and keep daily dashboard reminders trustworthy."),\"translatedDescription\":$(json_string "Backend role focused on workflow trust signals, saved application package reuse, and manual-send follow-up discipline."),\"descriptionLanguage\":$(json_string "en"),\"tags\":$(json_string "ASP.NET Core, React, SQLite, Workflow, Follow-up"),\"deadline\":$(json_string "2026-04-10T12:00:00Z"),\"coverLetterText\":$(json_string "Saved acceptance cover letter that references workflow trust signals, recruiter continuity, and measured backend delivery."),\"jobUrl\":$(json_string "$JOB_URL"),\"dateApplied\":$(json_string "$DATE_APPLIED"),\"feedbackRequestedAt\":null,\"hasResume\":true,\"hasCoverLetter\":true,\"hasPortfolio\":true,\"hasOtherAttachment\":false}"
|
||||
|
||||
if [[ -z "$job_id" ]]; then
|
||||
create_job_response="$TMP_DIR/create-job-response.json"
|
||||
request POST "job create" "$API_BASE/jobapplications" "$create_job_response" json "$create_job_body"
|
||||
job_id="$(json_extract "$create_job_response" 'data["id"]')"
|
||||
job_action="created"
|
||||
fi
|
||||
[[ -n "$job_id" ]] || fail "Job create/update flow did not yield a job id."
|
||||
|
||||
update_job_body="$TMP_DIR/update-job.json"
|
||||
write_json "$update_job_body" "{\"jobTitle\":$(json_string "$JOB_TITLE"),\"companyId\":$company_id,\"status\":$(json_string "Waiting"),\"responseReceived\":false,\"responseDate\":null,\"location\":$(json_string "Oslo / Hybrid"),\"salary\":$(json_string "NOK 900000"),\"nextAction\":$(json_string "Review saved package and send a manual recruiter follow-up from the linked thread."),\"followUpAt\":$(json_string "$FOLLOW_UP_AT"),\"hasResume\":true,\"hasCoverLetter\":true,\"hasPortfolio\":true,\"hasOtherAttachment\":false,\"notes\":$(json_string $'Acceptance seed notes for S06.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved acceptance application answer with concrete API and workflow evidence.\n<<<END_APPLICATION_ANSWER_DRAFT>>>'),\"description\":$(json_string "Lead the backend of an individual-first job tracking platform, maintain recruiter-thread continuity, and keep daily dashboard reminders trustworthy."),\"translatedDescription\":$(json_string "Backend role focused on workflow trust signals, saved application package reuse, and manual-send follow-up discipline."),\"descriptionLanguage\":$(json_string "en"),\"tags\":$(json_string "ASP.NET Core, React, SQLite, Workflow, Follow-up"),\"deadline\":$(json_string "2026-04-10T12:00:00Z"),\"coverLetterText\":$(json_string "Saved acceptance cover letter that references workflow trust signals, recruiter continuity, and measured backend delivery."),\"jobUrl\":$(json_string "$JOB_URL"),\"dateApplied\":$(json_string "$DATE_APPLIED"),\"feedbackRequestedAt\":null,\"statusChangedAt\":null}"
|
||||
update_job_response="$TMP_DIR/update-job-response.json"
|
||||
request PUT "job update" "$API_BASE/jobapplications/$job_id" "$update_job_response" empty "$update_job_body"
|
||||
|
||||
save_cv_body="$TMP_DIR/save-cv.json"
|
||||
write_json "$save_cv_body" "{\"tailoredCvText\":$(json_string "Saved acceptance tailored CV highlighting ASP.NET Core delivery, workflow trust signals, recruiter-thread continuity, and dashboard reminder ownership.")}"
|
||||
save_cv_response="$TMP_DIR/save-cv-response.json"
|
||||
request PUT "tailored CV save" "$API_BASE/jobapplications/$job_id/tailored-cv" "$save_cv_response" empty "$save_cv_body"
|
||||
|
||||
save_drafts_body="$TMP_DIR/save-drafts.json"
|
||||
write_json "$save_drafts_body" "{\"coverLetterText\":$(json_string "Saved acceptance cover letter that references workflow trust signals, recruiter continuity, and measured backend delivery."),\"notes\":$(json_string $'Acceptance seed notes for S06.\n\n<<<APPLICATION_ANSWER_DRAFT>>>\nSaved acceptance application answer with concrete API and workflow evidence.\n<<<END_APPLICATION_ANSWER_DRAFT>>>'),\"recruiterMessageDraft\":$(json_string "Saved acceptance recruiter message draft that acknowledges the prior thread and keeps the human send step explicit.")}"
|
||||
save_drafts_response="$TMP_DIR/save-drafts-response.json"
|
||||
request PUT "application drafts save" "$API_BASE/jobapplications/$job_id/application-drafts" "$save_drafts_response" empty "$save_drafts_body"
|
||||
|
||||
followup_body="$TMP_DIR/followup.json"
|
||||
write_json "$followup_body" "{\"followUpAt\":$(json_string "$FOLLOW_UP_AT")}"
|
||||
followup_response="$TMP_DIR/followup-response.json"
|
||||
request PATCH "follow-up schedule" "$API_BASE/jobapplications/$job_id/followup" "$followup_response" empty "$followup_body"
|
||||
|
||||
correspondence_list="$TMP_DIR/correspondence-list.json"
|
||||
request GET "correspondence list" "$API_BASE/correspondence/$job_id" "$correspondence_list" json
|
||||
existing_correspondence_id="$(json_extract "$correspondence_list" 'next((item["id"] for item in data if item.get("externalMessageId") == "s06-acceptance-message-1"), "")')"
|
||||
correspondence_action="existing"
|
||||
|
||||
if [[ -n "$existing_correspondence_id" ]]; then
|
||||
existing_subject="$(json_extract "$correspondence_list" 'next((item.get("subject") or "" for item in data if item.get("externalMessageId") == "s06-acceptance-message-1"), "")')"
|
||||
existing_content="$(json_extract "$correspondence_list" 'next((item.get("content") or "" for item in data if item.get("externalMessageId") == "s06-acceptance-message-1"), "")')"
|
||||
if [[ "$existing_subject" != "Backend Engineer follow-up" || "$existing_content" != "Hi Casey,\n\nThanks again for applying. We reviewed your saved package and would welcome a short manual follow-up next week so we can confirm timeline and next steps.\n\nBest,\nMaria" ]]; then
|
||||
delete_response="$TMP_DIR/delete-correspondence-response.json"
|
||||
request DELETE "correspondence delete" "$API_BASE/correspondence/$existing_correspondence_id" "$delete_response" empty
|
||||
existing_correspondence_id=""
|
||||
correspondence_action="replaced"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -z "$existing_correspondence_id" ]]; then
|
||||
create_correspondence_body="$TMP_DIR/create-correspondence.json"
|
||||
write_json "$create_correspondence_body" "{\"jobApplicationId\":$job_id,\"from\":$(json_string "Maria Recruiter"),\"content\":$(json_string $'Hi Casey,\n\nThanks again for applying. We reviewed your saved package and would welcome a short manual follow-up next week so we can confirm timeline and next steps.\n\nBest,\nMaria'),\"subject\":$(json_string "Backend Engineer follow-up"),\"channel\":$(json_string "email"),\"date\":$(json_string "$CORRESPONDENCE_AT"),\"externalMessageId\":$(json_string "$MESSAGE_ID"),\"externalThreadId\":$(json_string "$THREAD_ID"),\"externalFrom\":$(json_string "maria.recruiter@example.invalid"),\"externalTo\":$(json_string "casey@example.invalid")}"
|
||||
create_correspondence_response="$TMP_DIR/create-correspondence-response.json"
|
||||
request POST "correspondence create" "$API_BASE/correspondence" "$create_correspondence_response" json "$create_correspondence_body"
|
||||
existing_correspondence_id="$(json_extract "$create_correspondence_response" 'data["id"]')"
|
||||
if [[ "$correspondence_action" != "replaced" ]]; then
|
||||
correspondence_action="created"
|
||||
fi
|
||||
fi
|
||||
|
||||
request GET "job details" "$API_BASE/jobapplications/$job_id" "$TMP_DIR/job-details.json" json
|
||||
request GET "readiness" "$API_BASE/jobapplications/$job_id/readiness" "$TMP_DIR/readiness.json" json
|
||||
request GET "correspondence list final" "$API_BASE/correspondence/$job_id" "$TMP_DIR/correspondence-final.json" json
|
||||
|
||||
workflow_action="$(json_extract "$TMP_DIR/readiness.json" 'data["workflowSignal"]["actionKey"]')"
|
||||
readiness_score="$(json_extract "$TMP_DIR/readiness.json" 'data["score"]')"
|
||||
readiness_level="$(json_extract "$TMP_DIR/readiness.json" 'data["level"]')"
|
||||
correspondence_count="$(json_extract "$TMP_DIR/correspondence-final.json" 'len(data)')"
|
||||
completed_items="$(json_extract "$TMP_DIR/readiness.json" '" | ".join(data.get("completed", []))')"
|
||||
reminder_items="$(json_extract "$TMP_DIR/readiness.json" '" | ".join(data.get("reminders", []))')"
|
||||
|
||||
note "seed.result=success"
|
||||
note "seed.company.id=$company_id"
|
||||
note "seed.job.id=$job_id"
|
||||
note "seed.job.action=$job_action"
|
||||
note "seed.correspondence.id=$existing_correspondence_id"
|
||||
note "seed.correspondence.action=$correspondence_action"
|
||||
note "seed.correspondence.count=$correspondence_count"
|
||||
note "seed.readiness.score=$readiness_score"
|
||||
note "seed.readiness.level=$readiness_level"
|
||||
note "seed.workflow.action=$workflow_action"
|
||||
note "seed.followUp.at=$FOLLOW_UP_AT"
|
||||
note "seed.completed=$completed_items"
|
||||
note "seed.reminders=$reminder_items"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TARGET_SCRIPT="$SCRIPT_DIR/s06-acceptance-data.sh"
|
||||
TMP_DIR="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP_DIR"' EXIT
|
||||
|
||||
pass() {
|
||||
printf 'PASS: %s\n' "$1"
|
||||
}
|
||||
|
||||
fail() {
|
||||
printf 'FAIL: %s\n' "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_expect_fail() {
|
||||
local name="$1"
|
||||
local expected="$2"
|
||||
shift 2
|
||||
local out="$TMP_DIR/${name}.log"
|
||||
set +e
|
||||
"$@" >"$out" 2>&1
|
||||
local exit_code=$?
|
||||
set -e
|
||||
if [[ $exit_code -eq 0 ]]; then
|
||||
cat "$out" >&2
|
||||
fail "$name unexpectedly passed"
|
||||
fi
|
||||
if ! grep -Fq "$expected" "$out"; then
|
||||
cat "$out" >&2
|
||||
fail "$name did not mention expected text: $expected"
|
||||
fi
|
||||
pass "$name"
|
||||
}
|
||||
|
||||
run_expect_pass() {
|
||||
local name="$1"
|
||||
shift
|
||||
local out="$TMP_DIR/${name}.log"
|
||||
"$@" >"$out" 2>&1 || {
|
||||
cat "$out" >&2
|
||||
fail "$name failed"
|
||||
}
|
||||
pass "$name"
|
||||
}
|
||||
|
||||
run_expect_fail "missing-auth-token" "AUTH_TOKEN is required" env -u AUTH_TOKEN bash "$TARGET_SCRIPT"
|
||||
run_expect_fail "bad-auth-token" "AUTH_TOKEN appears invalid or lacks access" env AUTH_TOKEN="not-a-real-token" bash "$TARGET_SCRIPT"
|
||||
|
||||
if [[ -n "${TEST_AUTH_TOKEN:-}" ]]; then
|
||||
run_expect_pass "happy-path-first-run" env AUTH_TOKEN="$TEST_AUTH_TOKEN" bash "$TARGET_SCRIPT"
|
||||
run_expect_pass "happy-path-second-run" env AUTH_TOKEN="$TEST_AUTH_TOKEN" bash "$TARGET_SCRIPT"
|
||||
else
|
||||
printf 'SKIP: happy-path rerun requires TEST_AUTH_TOKEN.\n'
|
||||
fi
|
||||
Reference in New Issue
Block a user