Avoid false session-expiry labels for validation errors

The toss wrapper now treats bare validation_error text as an upstream command failure instead of a session-expired signal. Structured auth doctor JSON remains the source of truth for empty portfolio/watchlist invalid-session promotion, while known stored-session-invalid stderr still maps to TossSessionExpiredError.\n\nConstraint: PR #192 follow-up must stay scoped to issue #126 toss-securities behavior.\nRejected: Keep validation_error in the global regex | it mislabels auth doctor transport failures and quote 403 validation errors as session expiry.\nConfidence: high\nScope-risk: narrow\nDirective: Do not broaden the free-text session classifier without regressions for auth doctor and quote upstream validation failures.\nTested: npm run lint --workspace toss-securities; npm run test --workspace toss-securities; npm run ci; manual mock tossctl validation_error checks; architect verification CLEAR\nNot-tested: Live tossctl network/auth session against real Toss upstream
This commit is contained in:
Jeffrey (Dongkyu) Kim 2026-04-30 02:30:18 +09:00
commit 8243e231db
2 changed files with 52 additions and 1 deletions

View file

@ -7,7 +7,7 @@ const {
} = require("./parse");
const execFile = util.promisify(childProcess.execFile);
const SESSION_EXPIRED_PATTERN = /stored session is no longer valid|validation_error/iu;
const SESSION_EXPIRED_PATTERN = /stored session is no longer valid/iu;
class TossSessionExpiredError extends Error {
constructor(message, details = {}) {

View file

@ -6,6 +6,7 @@ const path = require("node:path");
const {
buildReadOnlyCommand,
checkSession,
getAccountSummary,
getPortfolioPositions,
getQuote,
@ -267,3 +268,53 @@ exit 1
/issues\/15/.test(error.message)
);
});
test("checkSession treats auth doctor validation_error failures as inconclusive command errors", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-check-session-fail-"));
const binDir = path.join(tempDir, "bin");
fs.mkdirSync(binDir, { recursive: true });
const script = `#!/bin/sh
if [ "$3" = "auth" ] && [ "$4" = "doctor" ]; then
echo "validation_error: transport failure" 1>&2
exit 1
fi
printf '{"ok":true}\n'
`;
const binPath = path.join(binDir, "tossctl");
fs.writeFileSync(binPath, script, { mode: 0o755 });
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
await assert.rejects(
checkSession({ env }),
(error) =>
!(error instanceof TossSessionExpiredError) &&
/validation_error: transport failure/.test(error.message)
);
});
test("quote search stocks 403 with validation_error remains a non-session upstream error", async () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "toss-securities-403-validation-error-"));
const binDir = path.join(tempDir, "bin");
fs.mkdirSync(binDir, { recursive: true });
const script = `#!/bin/sh
echo "validation_error: status 403 at search/stocks" 1>&2
exit 1
`;
const binPath = path.join(binDir, "tossctl");
fs.writeFileSync(binPath, script, { mode: 0o755 });
const env = { ...process.env, PATH: `${binDir}:${process.env.PATH || ""}` };
await assert.rejects(
getQuote("ALM", { env }),
(error) =>
!(error instanceof TossSessionExpiredError) &&
/validation_error: status 403 at search\/stocks/.test(error.message) &&
/issues\/15/.test(error.message)
);
});