b37c0222a6
- "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
295 lines
16 KiB
Bash
Executable File
295 lines
16 KiB
Bash
Executable File
#!/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"
|