Export linked grader hint feedback#780
Conversation
Co-authored-by: Jonathan Bell <jon@jonbell.net>
WalkthroughThis PR introduces server-side pepper-based HMAC tokenization for assessment exports, adds new CLI options for selective export (skip gradebook, include all submissions), refactors the export pipeline into sectioned streams with error-pin engagement tracking, and improves transient stream error detection. ChangesAssessment Export with Server-Side Pepper Tokenization
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Co-authored-by: Jonathan Bell <jon@jonbell.net>
|
The latest updates on your projects. Learn more about Argos notifications ↗︎
|
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
supabase/functions/cli/commands/assessment.ts (1)
1373-1384:⚠️ Potential issue | 🟠 Major | ⚡ Quick winFix
test_output_max_bytestruncation to use UTF-8 byte length (notstring.length) inassessment.ts(supabase/functions/cli/commands/assessment.ts:1373-1384)
row.outputis astring | null, but the code usesoutput.lengthandoutput.slice(0, outputMaxBytes), which count UTF-16 code units—not UTF-8 bytes. This makes both:
record.output_truncateddecision incorrect for non-ASCII outputrecord.output_full_bytes = output.lengthincorrectTruncate by UTF-8 byte length (e.g., iterate with
TextEncoder/TextDecoderor equivalent) and setoutput_full_bytesto the UTF-8 byte length.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@supabase/functions/cli/commands/assessment.ts` around lines 1373 - 1384, The truncation logic in assessment.ts currently uses string.length and slice which count UTF-16 code units; change it to compute the UTF-8 byte length of row.output (use TextEncoder to get a Uint8Array), set record.output_full_bytes to that byte length, and when output exceeds outputMaxBytes truncate by UTF-8 bytes (slice the encoded bytes to outputMaxBytes and decode back to a string with TextDecoder) so record.output contains a properly truncated UTF-8-safe string and record.output_truncated is set accordingly; update the branches that reference output, outputMaxBytes, record.output, record.output_truncated, and record.output_full_bytes to use this byte-based approach.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@cli/commands/assessment/export.ts`:
- Around line 520-530: The pagination loop using batchIndex and the returned
next cursor (from streamAssignmentSection ->
page.endRecord.next_score_review_batch_index) must guard against non-advancing
cursors; update the loop to break (or throw) when next is not a number OR when
next <= batchIndex to prevent infinite loops and repeated edge calls. Do the
same change for the sibling loops that read next_answer_review_batch_index /
other next_*_batch_index cursors (the loops that call mergeBuckets,
addPageCounts, assertExpectedCount), i.e. only assign batchIndex = next when
typeof next === "number" && next > batchIndex, otherwise exit the loop
(optionally log a warning).
In `@supabase/functions/cli/commands/assessment.ts`:
- Around line 758-773: The switch over section (derived from params.section in
assessment.ts) currently treats unknown values as "all" via the default case,
which causes typos like "score" to run exportAssignmentAll; change this to
explicitly reject invalid section values by validating params.section (or the
computed const section) against the allowed AssignmentExportSection values and
replacing the switch default with logic that throws/returns a 400 Bad Request
(or calls the existing request-error helper) when the value is not one of
"meta","submissions","scores","tests","engagement","all"; update the switch in
the same function to only handle the known cases (exportAssignmentMeta,
exportAssignmentSubmissions, exportAssignmentScores, exportAssignmentTests,
exportAssignmentEngagement, exportAssignmentAll) and ensure unknown inputs
produce an immediate error response instead of falling back to
exportAssignmentAll.
- Around line 454-468: slimExtraDataForExport currently destructures prompt and
result into _prompt/_result but leaves them unused which trips
`@typescript-eslint/no-unused-vars`; after the destructuring in
slimExtraDataForExport (where llmObj and { prompt: _prompt, result: _result,
...llmRest } are declared) explicitly mark those vars as used by adding short
no-op references (for example: void _prompt; void _result;) before the return so
ESLint stops complaining while preserving the existing shape logic for llmObj
and llmRest.
In `@tests/unit/assessment-export-tokenization.test.ts`:
- Line 13: Update the import statement in the test to use the project-root alias
instead of the relative path: replace the relative import that currently pulls
createTokenizer, generateRandomSalt, and base32 from
"../../supabase/functions/cli/utils/tokenization" with the alias import from
"`@/supabase/functions/cli/utils/tokenization`" so the test imports
createTokenizer, generateRandomSalt, and base32 via the "`@/`..." alias.
---
Outside diff comments:
In `@supabase/functions/cli/commands/assessment.ts`:
- Around line 1373-1384: The truncation logic in assessment.ts currently uses
string.length and slice which count UTF-16 code units; change it to compute the
UTF-8 byte length of row.output (use TextEncoder to get a Uint8Array), set
record.output_full_bytes to that byte length, and when output exceeds
outputMaxBytes truncate by UTF-8 bytes (slice the encoded bytes to
outputMaxBytes and decode back to a string with TextDecoder) so record.output
contains a properly truncated UTF-8-safe string and record.output_truncated is
set accordingly; update the branches that reference output, outputMaxBytes,
record.output, record.output_truncated, and record.output_full_bytes to use this
byte-based approach.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 87d5ec32-d86b-420a-987e-d84f6df2646f
📒 Files selected for processing (8)
cli/commands/assessment/deanonymize.tscli/commands/assessment/export.tscli/utils/transientRetry.tssupabase/functions/cli/commands/assessment.tssupabase/functions/cli/utils/tokenization.tssupabase/migrations/20260522143000_assessment_export_pepper.sqltests/unit/assessment-export-tokenization.test.tstests/unit/cli-transient-retry.test.ts
| for (let batchIndex = 0; ; ) { | ||
| const page = await streamAssignmentSection(baseParams, slug, "scores", { | ||
| score_review_batch_index: batchIndex | ||
| }); | ||
| mergeBuckets(page.buckets); | ||
| addPageCounts(page.endRecord); | ||
| assertExpectedCount(page.endRecord, "scores", page.buckets.score!.length); | ||
| const next = page.endRecord.next_score_review_batch_index; | ||
| if (typeof next !== "number") break; | ||
| batchIndex = next; | ||
| } |
There was a problem hiding this comment.
Guard pagination cursors against non-advancing next_*_batch_index values.
At Line 528, Line 541, and Line 555, the loop only checks whether next is a number. If the server returns the same (or lower) batch index, these loops can run indefinitely and repeatedly hit the edge function.
Proposed fix
for (let batchIndex = 0; ; ) {
const page = await streamAssignmentSection(baseParams, slug, "scores", {
score_review_batch_index: batchIndex
});
@@
const next = page.endRecord.next_score_review_batch_index;
if (typeof next !== "number") break;
+ if (next <= batchIndex) {
+ throw new CLIError(
+ `assignment ${slug}: non-advancing next_score_review_batch_index (${next}) after ${batchIndex}`
+ );
+ }
batchIndex = next;
}
@@
for (let batchIndex = 0; ; ) {
const page = await streamAssignmentSection(baseParams, slug, "tests", {
test_submission_batch_index: batchIndex
});
@@
const next = page.endRecord.next_test_submission_batch_index;
if (typeof next !== "number") break;
+ if (next <= batchIndex) {
+ throw new CLIError(
+ `assignment ${slug}: non-advancing next_test_submission_batch_index (${next}) after ${batchIndex}`
+ );
+ }
batchIndex = next;
}
@@
for (let batchIndex = 0; ; ) {
const page = await streamAssignmentSection(baseParams, slug, "engagement", {
engagement_submission_batch_index: batchIndex
});
@@
const next = page.endRecord.next_engagement_submission_batch_index;
if (typeof next !== "number") break;
+ if (next <= batchIndex) {
+ throw new CLIError(
+ `assignment ${slug}: non-advancing next_engagement_submission_batch_index (${next}) after ${batchIndex}`
+ );
+ }
batchIndex = next;
}Also applies to: 533-543, 546-557
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@cli/commands/assessment/export.ts` around lines 520 - 530, The pagination
loop using batchIndex and the returned next cursor (from streamAssignmentSection
-> page.endRecord.next_score_review_batch_index) must guard against
non-advancing cursors; update the loop to break (or throw) when next is not a
number OR when next <= batchIndex to prevent infinite loops and repeated edge
calls. Do the same change for the sibling loops that read
next_answer_review_batch_index / other next_*_batch_index cursors (the loops
that call mergeBuckets, addPageCounts, assertExpectedCount), i.e. only assign
batchIndex = next when typeof next === "number" && next > batchIndex, otherwise
exit the loop (optionally log a warning).
| function slimExtraDataForExport(extra: unknown): unknown { | ||
| if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null; | ||
| const data = extra as Record<string, unknown>; | ||
| const llm = data.llm; | ||
| if (llm === null || llm === undefined || typeof llm !== "object") return extra; | ||
| const llmObj = llm as Record<string, unknown>; | ||
| const { prompt: _prompt, result: _result, ...llmRest } = llmObj; | ||
| return { | ||
| ...data, | ||
| llm: { | ||
| ...llmRest, | ||
| has_prompt: typeof llmObj.prompt === "string" && llmObj.prompt.length > 0, | ||
| has_result: typeof llmObj.result === "string" && llmObj.result.trim().length > 0 | ||
| } | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the helper + surrounding logic
FILE="supabase/functions/cli/commands/assessment.ts"
sed -n '420,520p' "$FILE" | nl -ba | sed -n '1,120p'
# Find where handleAssignmentExport sets/validates `section`
rg -n "handleAssignmentExport|section" "$FILE" -S
# Find truncation logic using outputMaxBytes/output_full_bytes
rg -n "outputMaxBytes|output_full_bytes|truncate" "$FILE" -S
# Also inspect any test/output code paths nearby
sed -n '520,700p' "$FILE" | nl -ba | sed -n '1,200p'Repository: pawtograder/platform
Length of output: 107
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="supabase/functions/cli/commands/assessment.ts"
echo "=== Code around slimExtraDataForExport (approx 430-510) ==="
sed -n '430,510p' "$FILE" | awk '{print NR ":" $0}'
echo "=== Code around handleAssignmentExport section logic (approx 380-520) ==="
sed -n '380,520p' "$FILE" | awk '{print NR ":" $0}'
echo "=== Search for section handling ==="
rg -n "handleAssignmentExport|section" "$FILE" -S || true
echo "=== Search for outputMaxBytes / output_full_bytes truncation ==="
rg -n "outputMaxBytes|output_full_bytes|truncate|full_bytes" "$FILE" -S || true
echo "=== Code around truncation vars (approx 520-650) ==="
sed -n '520,650p' "$FILE" | awk '{print NR ":" $0}'Repository: pawtograder/platform
Length of output: 18272
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE="supabase/functions/cli/commands/assessment.ts"
echo "=== Code around handleAssignmentExport switch (approx 730-820) ==="
sed -n '730,820p' "$FILE" | awk '{print NR ":" $0}'
echo "=== Code around truncation logic (approx 1200-1420) ==="
sed -n '1220,1420p' "$FILE" | awk '{print NR ":" $0}'
echo "=== Locate eslint no-unused-vars config ==="
ls -1 .eslintrc* eslint* package.json 2>/dev/null || true
rg -n "`@typescript-eslint/no-unused-vars`|no-unused-vars" .eslintrc* eslint* package.json . 2>/dev/null | head -n 50
echo "=== Find any rule override pattern for unused vars starting with '_' ==="
rg -n "argsIgnorePattern|varsIgnorePattern|ignoreRestSiblings|underscore" .eslintrc* eslint* package.json . 2>/dev/null | head -n 50Repository: pawtograder/platform
Length of output: 13088
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Show relevant eslint rule config
if [ -f eslint.config.mjs ]; then
echo "=== eslint.config.mjs (relevant no-unused-vars rules) ==="
rg -n "no-unused-vars|`@typescript-eslint/no-unused-vars`|varsIgnorePattern|argsIgnorePattern|ignoreRestSiblings" eslint.config.mjs
sed -n '1,220p' eslint.config.mjs | awk '{print NR ":" $0}' | head -n 220
# Also show around any matches
MATCHES=$(rg -n "no-unused-vars|`@typescript-eslint/no-unused-vars`" eslint.config.mjs | cut -d: -f1 | head -n 20 || true)
for L in $MATCHES; do
echo "=== Context around eslint.config.mjs:$L ==="
sed -n "$((L-10)),$((L+20))p" eslint.config.mjs | awk '{print NR ":" $0}' | head -n 60
done
fi
FILE="supabase/functions/cli/commands/assessment.ts"
echo "=== Locate where handleAssignmentExport is wired / params schema ==="
rg -n "handler:\s*handleAssignmentExport|handleAssignmentExport\(|section\?|\bsection\b" "$FILE" -S
echo "=== Inspect around the CLI command definition (near handler) ==="
LINE=$(rg -n "handler:\s*handleAssignmentExport" "$FILE" -n | head -n 1 | cut -d: -f1 || true)
if [ -n "${LINE:-}" ]; then
START=$((LINE-120))
END=$((LINE+120))
sed -n "${START},${END}p" "$FILE" | awk '{print NR ":" $0}'
fi
echo "=== Search for schema/validation (zod/parse) for section ==="
rg -n "zod|schema|parse|safeParse|validate|AssignmentExportSection|section.*(zod|parse|safeParse)" "$FILE" -S || trueRepository: pawtograder/platform
Length of output: 122
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== eslint.config.mjs length ==="
wc -l eslint.config.mjs 2>/dev/null || true
echo "=== eslint.config.mjs top 220 lines ==="
sed -n '1,220p' eslint.config.mjs 2>/dev/null | awk '{print NR ":" $0}' | head -n 220
echo "=== eslint.config.mjs full search for unused-vars config knobs ==="
rg -n "no-unused-vars|`@typescript-eslint/no-unused-vars`|varsIgnorePattern|argsIgnorePattern|ignoreRestSiblings|underscore|\\^_" eslint.config.mjs || true
echo "=== Search for section runtime validation in assessment.ts ==="
FILE="supabase/functions/cli/commands/assessment.ts"
rg -n "safeParse|zod|schema|parse\\(|validate\\(|enum\\(|AssignmentExportSection|section\\s*:" "$FILE" -S || true
echo "=== Find CLI option definitions around handleAssignmentExport wiring ==="
rg -n "handler:\s*handleAssignmentExport|handleAssignmentExport" "$FILE" -S
LINE=$(rg -n "handler:\s*handleAssignmentExport" "$FILE" -S | head -n 1 | cut -d: -f1)
if [ -n "${LINE:-}" ]; then
START=$((LINE-160)); END=$((LINE+160));
echo "=== assessment.ts around command registration (~$START-$END) ==="
sed -n "${START},${END}p" "$FILE" | awk '{print NR ":" $0}' | head -n 260
fi
echo "=== Search for test_output_max_bytes docs/usage ==="
rg -n "test_output_max_bytes|outputMaxBytes|output_full_bytes|output\\.length" "$FILE" -SRepository: pawtograder/platform
Length of output: 14749
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Find any eslint rule config that mentions no-unused-vars / varsIgnorePattern / argsIgnorePattern
rg -n --hidden --glob '!.git/**' \
"no-unused-vars|`@typescript-eslint/no-unused-vars`|varsIgnorePattern|argsIgnorePattern" \
. -S || true
# 2) Inspect DEFAULT_TEST_OUTPUT_MAX_BYTES definition and surrounding docs
FILE="supabase/functions/cli/commands/assessment.ts"
rg -n "DEFAULT_TEST_OUTPUT_MAX_BYTES" "$FILE" -S
# print near the definition (top of file)
TOPLINE=$(rg -n "DEFAULT_TEST_OUTPUT_MAX_BYTES" "$FILE" -S | head -n 1 | cut -d: -f1)
START=$((TOPLINE-40)); END=$((TOPLINE+60))
sed -n "${START},${END}p" "$FILE" | awk '{print NR ":" $0}'
# 3) Show the exact helper line for destructuring to ensure the unused vars are truly _prompt/_result only.
sed -n '40,110p' "$FILE" | awk '{print NR ":" $0}' | cat
rg -n "slimExtraDataForExport|_prompt|_result|has_prompt|has_result" "$FILE" -SRepository: pawtograder/platform
Length of output: 12614
Fix ESLint unused vars in slimExtraDataForExport (supabase/functions/cli/commands/assessment.ts, ~lines 454-468): _prompt/_result are destructured but never used, so @typescript-eslint/no-unused-vars trips on the destructuring line.
Proposed fix
function slimExtraDataForExport(extra: unknown): unknown {
if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null;
const data = extra as Record<string, unknown>;
const llm = data.llm;
if (llm === null || llm === undefined || typeof llm !== "object") return extra;
const llmObj = llm as Record<string, unknown>;
- const { prompt: _prompt, result: _result, ...llmRest } = llmObj;
+ const llmRest = { ...llmObj };
+ const hasPrompt = typeof llmRest.prompt === "string" && llmRest.prompt.length > 0;
+ const hasResult = typeof llmRest.result === "string" && llmRest.result.trim().length > 0;
+ delete llmRest.prompt;
+ delete llmRest.result;
return {
...data,
llm: {
...llmRest,
- has_prompt: typeof llmObj.prompt === "string" && llmObj.prompt.length > 0,
- has_result: typeof llmObj.result === "string" && llmObj.result.trim().length > 0
+ has_prompt: hasPrompt,
+ has_result: hasResult
}
};
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function slimExtraDataForExport(extra: unknown): unknown { | |
| if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null; | |
| const data = extra as Record<string, unknown>; | |
| const llm = data.llm; | |
| if (llm === null || llm === undefined || typeof llm !== "object") return extra; | |
| const llmObj = llm as Record<string, unknown>; | |
| const { prompt: _prompt, result: _result, ...llmRest } = llmObj; | |
| return { | |
| ...data, | |
| llm: { | |
| ...llmRest, | |
| has_prompt: typeof llmObj.prompt === "string" && llmObj.prompt.length > 0, | |
| has_result: typeof llmObj.result === "string" && llmObj.result.trim().length > 0 | |
| } | |
| }; | |
| function slimExtraDataForExport(extra: unknown): unknown { | |
| if (extra === null || extra === undefined || typeof extra !== "object") return extra ?? null; | |
| const data = extra as Record<string, unknown>; | |
| const llm = data.llm; | |
| if (llm === null || llm === undefined || typeof llm !== "object") return extra; | |
| const llmObj = llm as Record<string, unknown>; | |
| const llmRest = { ...llmObj }; | |
| const hasPrompt = typeof llmRest.prompt === "string" && llmRest.prompt.length > 0; | |
| const hasResult = typeof llmRest.result === "string" && llmRest.result.trim().length > 0; | |
| delete llmRest.prompt; | |
| delete llmRest.result; | |
| return { | |
| ...data, | |
| llm: { | |
| ...llmRest, | |
| has_prompt: hasPrompt, | |
| has_result: hasResult | |
| } | |
| }; | |
| } |
🧰 Tools
🪛 ESLint
[error] 460-460: '_prompt' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
[error] 460-460: '_result' is assigned a value but never used.
(@typescript-eslint/no-unused-vars)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/functions/cli/commands/assessment.ts` around lines 454 - 468,
slimExtraDataForExport currently destructures prompt and result into
_prompt/_result but leaves them unused which trips
`@typescript-eslint/no-unused-vars`; after the destructuring in
slimExtraDataForExport (where llmObj and { prompt: _prompt, result: _result,
...llmRest } are declared) explicitly mark those vars as used by adding short
no-op references (for example: void _prompt; void _result;) before the return so
ESLint stops complaining while preserving the existing shape logic for llmObj
and llmRest.
| const section: AssignmentExportSection = params.section ?? "all"; | ||
|
|
||
| switch (section) { | ||
| case "meta": | ||
| return exportAssignmentMeta(exportCtx); | ||
| case "submissions": | ||
| return exportAssignmentSubmissions(exportCtx); | ||
| case "scores": | ||
| return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0)); | ||
| case "tests": | ||
| return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0)); | ||
| case "engagement": | ||
| return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0)); | ||
| case "all": | ||
| default: | ||
| return exportAssignmentAll(exportCtx); |
There was a problem hiding this comment.
Reject unknown section values instead of falling back to "all".
A typo like "score" currently lands in default and runs the monolithic export path. That changes behavior and can trigger the most expensive stream instead of returning a 400.
Proposed fix
switch (section) {
case "meta":
return exportAssignmentMeta(exportCtx);
case "submissions":
return exportAssignmentSubmissions(exportCtx);
case "scores":
return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0));
case "tests":
return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0));
case "engagement":
return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0));
case "all":
- default:
return exportAssignmentAll(exportCtx);
+ default:
+ throw new CLICommandError(`invalid section: ${String(params.section)}`, 400);
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const section: AssignmentExportSection = params.section ?? "all"; | |
| switch (section) { | |
| case "meta": | |
| return exportAssignmentMeta(exportCtx); | |
| case "submissions": | |
| return exportAssignmentSubmissions(exportCtx); | |
| case "scores": | |
| return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0)); | |
| case "tests": | |
| return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0)); | |
| case "engagement": | |
| return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0)); | |
| case "all": | |
| default: | |
| return exportAssignmentAll(exportCtx); | |
| const section: AssignmentExportSection = params.section ?? "all"; | |
| switch (section) { | |
| case "meta": | |
| return exportAssignmentMeta(exportCtx); | |
| case "submissions": | |
| return exportAssignmentSubmissions(exportCtx); | |
| case "scores": | |
| return exportAssignmentScores(exportCtx, Math.max(0, params.score_review_batch_index ?? 0)); | |
| case "tests": | |
| return exportAssignmentTests(exportCtx, Math.max(0, params.test_submission_batch_index ?? 0)); | |
| case "engagement": | |
| return exportAssignmentEngagement(exportCtx, Math.max(0, params.engagement_submission_batch_index ?? 0)); | |
| case "all": | |
| return exportAssignmentAll(exportCtx); | |
| default: | |
| throw new CLICommandError(`invalid section: ${String(params.section)}`, 400); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@supabase/functions/cli/commands/assessment.ts` around lines 758 - 773, The
switch over section (derived from params.section in assessment.ts) currently
treats unknown values as "all" via the default case, which causes typos like
"score" to run exportAssignmentAll; change this to explicitly reject invalid
section values by validating params.section (or the computed const section)
against the allowed AssignmentExportSection values and replacing the switch
default with logic that throws/returns a 400 Bad Request (or calls the existing
request-error helper) when the value is not one of
"meta","submissions","scores","tests","engagement","all"; update the switch in
the same function to only handle the known cases (exportAssignmentMeta,
exportAssignmentSubmissions, exportAssignmentScores, exportAssignmentTests,
exportAssignmentEngagement, exportAssignmentAll) and ensure unknown inputs
produce an immediate error response instead of falling back to
exportAssignmentAll.
| * produce identical tokens for the same (kind, raw_id). | ||
| */ | ||
|
|
||
| import { createTokenizer, generateRandomSalt, base32 } from "../../supabase/functions/cli/utils/tokenization"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Show the import line in the target test file
echo "---- File snippet: tests/unit/assessment-export-tokenization.test.ts (lines ~1-30) ----"
nl -ba tests/unit/assessment-export-tokenization.test.ts | sed -n '1,40p'
# 2) Find tsconfig path alias configuration for `@/`*
echo "---- tsconfig/jsconfig paths for `@/`* ----"
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`\*"' -S . || true
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`*"' -S . || true
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' 'paths' -S tsconfig*.json jsconfig*.json || true
# 3) Check existing imports from supabase/functions/cli/utils/tokenization across the repo
echo "---- Usages of createTokenizer/generateRandomSalt/base32 import ----"
rg -n "supabase/functions/cli/utils/tokenization" -S . || true
# 4) Check for `@/` usage patterns in tests to see if this convention is actually followed
echo "---- Example `@/`* imports in tests (up to 50 matches) ----"
rg -n "from \"`@/`" -S tests | head -n 50 || true
# 5) Check whether other imports from the same supabase/functions directory use relative paths or `@/`
echo "---- Imports from supabase/functions/* (relative vs `@/`), limited to tests ----"
rg -n "(from \"\\.{1,2}/supabase/|from \"`@/supabase/`)" -S tests || trueRepository: pawtograder/platform
Length of output: 213
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "---- File snippet: tests/unit/assessment-export-tokenization.test.ts (lines 1-40) ----"
cat -n tests/unit/assessment-export-tokenization.test.ts | sed -n '1,40p'
echo "---- tsconfig/jsconfig paths for `@/`* (searching) ----"
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`\*"' -S . || true
rg -n --hidden --glob 'tsconfig*.json' '\"`@/`\\*\"|\"@\\*\"|paths' -S . || true
rg -n --hidden --glob 'tsconfig*.json' --glob 'jsconfig*.json' '"`@/`\*"' -S tsconfig*.json jsconfig*.json 2>/dev/null || true
echo "---- Usages of supabase/functions/cli/utils/tokenization import ----"
rg -n "supabase/functions/cli/utils/tokenization" -S . || true
echo "---- Example `@/`* imports in tests (up to 50 matches) ----"
rg -n "from \"`@/`" -S tests | head -n 50 || true
echo "---- Imports from supabase/functions/* (relative vs `@/`), limited to tests ----"
rg -n "(from \"\\.{1,2}/supabase/|from \"`@/supabase/`)" -S tests || trueRepository: pawtograder/platform
Length of output: 8703
Use @/ alias for this test import
Replace the relative import with the project-root alias:
import { createTokenizer, generateRandomSalt, base32 } from "`@/supabase/functions/cli/utils/tokenization`";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/unit/assessment-export-tokenization.test.ts` at line 13, Update the
import statement in the test to use the project-root alias instead of the
relative path: replace the relative import that currently pulls createTokenizer,
generateRandomSalt, and base32 from
"../../supabase/functions/cli/utils/tokenization" with the alias import from
"`@/supabase/functions/cli/utils/tokenization`" so the test imports
createTokenizer, generateRandomSalt, and base32 via the "`@/`..." alias.
Summary
grader_result_testsids sohints.jsononly contains rows linked to exported tests.extra_datafield on emitted grader test records.error-pin-engagement.json.Testing
npm run formatnpm run lintnpm test -- --runTestsByPath tests/unit/assessment-export-tokenization.test.ts tests/unit/assessment-export-selectors.test.tsnode -e "const fs=require('fs'); const edge=fs.readFileSync('supabase/functions/cli/commands/assessment.ts','utf8'); const cli=fs.readFileSync('cli/commands/assessment/export.ts','utf8'); if(!edge.includes('kind: \\\"error_pin_engagement\\\"')) throw new Error('edge stream does not emit error_pin_engagement'); if(!edge.includes('.from(\\\"discussion_thread_read_status\\\")')) throw new Error('read status is not exported'); if(!edge.includes('.from(\\\"discussion_thread_likes\\\")')) throw new Error('likes are not exported'); if(!cli.includes('error-pin-engagement.json')) throw new Error('CLI does not write error-pin-engagement.json'); console.log('assessment export includes error-pin engagement read/like wiring');"Notes
npx -y deno check cli/commands/assessment.tswas attempted; it reaches the function but fails on the generatedsupabase/functions/_shared/SupabaseTypes.d.tsexport const Constantsambient declaration before checking this patch.npx tsc --noEmit --pretty falsewas attempted; it fails on the pre-existingtests/e2e/active-submission-gradebook-db.spec.ts(49,5)nullable string assignment.Summary by CodeRabbit
New Features
--skip-gradebookoption to exclude gradebook data from exports.--all-submissionsoption to export all submission attempts.Improvements