QA: Manual SDK feature test plan

A copy-pasteable checklist a QA engineer runs top-to-bottom in under an hour to verify every manual-facing SDK feature against the real (sandboxed) engine.

QA: Manual SDK feature test plan

This is the human-facing acceptance checklist for the ISA TypeScript SDK
(isa-sdk). It complements the automated dev tests (unit + cross-language
conformance) that QA does not run. Work through it top to bottom; the whole
plan takes well under an hour.

Each feature is a Setup → Steps → Expected triple. Paste the code,
run it, and confirm the output matches the Expected block. When an
output line is marked // varies, the exact value depends on which carriers
are enabled on your account — confirm the shape, not the literal value.

What "test mode" means. Every call below runs against the real
underwriting engine with real carrier rules. Pricing and audit are
sandboxed, so nothing you do here touches production data or bills a
customer. Switching ISA_TOKEN to a live token is the only change needed
to promote — do not do that during QA.


Prerequisite — your test token

You need a valid isa_test_… bearer token, issued during onboarding and
delivered in the checkout email. Export it before you start:

export ISA_TOKEN="<paste your isa_test_… token here>"
echo "Token prefix: ${ISA_TOKEN:0:9}"

Expected

Token prefix: isa_test_

If the prefix is not isa_test_, stop — you are about to run QA against
live mode. Re-export the test token.

The persona used throughout is the canonical docs applicant: John Doe,
NC, born 1962-04-18, male, 5'10", 195 lbs, no nicotine, no conditions.


1. Install and authenticate

Setup — a scratch directory with the SDK installed.

mkdir isa-qa && cd isa-qa
npm init -y
npm install isa-sdk
npm install --save-dev tsx typescript @types/node

Steps — create 01-auth.ts and run it with npx tsx 01-auth.ts.

import { Isa, IsaConfigError } from 'isa-sdk';

// withBearer() reads ISA_TOKEN from the environment.
let isa: Isa;
try {
  isa = await Isa.withBearer();
} catch (err) {
  if (err instanceof IsaConfigError) {
    console.error('Configuration error:', err.message);
    process.exit(1);
  }
  throw err;
}

console.log('Client created:', isa.zyins !== undefined);

Expected

Client created: true

A thrown IsaConfigError means ISA_TOKEN is unset — re-check the
prerequisite step.


2. Prequalify — John Doe (NC)

Setup — the authenticated client from step 1.

Steps — create 02-prequalify.ts and run it.

import {
  Isa,
  Sex,
  Height,
  Weight,
  NicotineDuration,
  Coverage,
  ProductSelection,
  Products,
} from 'isa-sdk';

const isa = await Isa.withBearer();

const { data } = await isa.zyins.prequalify({
  applicant: {
    dob: '1962-04-18',
    sex: Sex.Male,
    height: Height.fromFeetInches(5, 10),
    weight: Weight.fromPounds(195),
    state: 'NC',
    nicotineUse: { lastUsed: NicotineDuration.Never },
  },
  coverage: Coverage.faceValue(25_000),
  products: ProductSelection.of([Products.Fex.AetnaAccendo]),
});

console.log('plans is array:', Array.isArray(data.plans));
console.log('plan count:', data.plans.length);

const first = data.plans[0];
const primaryRow = first.pricing.find((row) => row.primary);
console.log('pricing is array:', Array.isArray(first.pricing));
console.log('has primary row:', primaryRow !== undefined);
console.log('premium cents is integer:', Number.isInteger(primaryRow?.premium?.amount.cents));

Expected — every boolean is true; the count is at least 1. Premium
cents is a whole number (integer minor units), never a float.

plans is array: true
plan count: 1            // varies
pricing is array: true
has primary row: true
premium cents is integer: true

If you get zero plans: the engine returned a valid response but no
carrier accepted the applicant. That is a valid result, not a failure — try
Coverage.faceValue(10_000) to confirm a non-empty list is reachable.


3. Quote — same shape as prequalify

Setup — the authenticated client.

Steps — create 03-quote.ts and run it.

import {
  Isa,
  Sex,
  Height,
  Weight,
  NicotineDuration,
  Coverage,
  ProductSelection,
  Products,
} from 'isa-sdk';

const isa = await Isa.withBearer();

const { data } = await isa.zyins.quote({
  applicant: {
    dob: '1962-04-18',
    sex: Sex.Male,
    height: Height.fromFeetInches(5, 10),
    weight: Weight.fromPounds(195),
    state: 'NC',
    nicotineUse: { lastUsed: NicotineDuration.Never },
  },
  coverage: Coverage.faceValue(25_000),
  products: ProductSelection.of([Products.Fex.AetnaAccendo]),
});

const primaryRow = data.plans[0]?.pricing.find((row) => row.primary);
console.log('plans is array:', Array.isArray(data.plans));
console.log('has primary row:', primaryRow !== undefined);
console.log('premium cents is integer:', Number.isInteger(primaryRow?.premium?.amount.cents));

Expected — identical shape to prequalify: a plans[] array, each plan
with a pricing[] table and a primary row carrying integer-cents premium.

plans is array: true
has primary row: true
premium cents is integer: true

4. Datasets — the reference bundle

Setup — the authenticated client. datasets.get() fetches the
condition/medication/nicotine reference bundle that powers match,
autocomplete, and autocorrect. Several later steps depend on this call
having run.

Steps — create 04-datasets.ts and run it.

import { Isa } from 'isa-sdk';

const isa = await Isa.withBearer();
const bundle = await isa.zyins.datasets.get();

console.log('conditions present:', bundle.conditions.length > 0);
console.log('medications present:', bundle.medications.length > 0);
console.log('nicotine options present:', bundle.nicotineOptions.length > 0);

Expected — all three lists are non-empty.

conditions present: true
medications present: true
nicotine options present: true

5. Match — text resolves to a Concept

Setup — the bundle must be loaded first (datasets.get()), so match has
a catalog to resolve against. match() is synchronous and never throws —
an unknown term returns an UnknownConcept with isKnown: false, never an
error.

Steps — create 05-match.ts and run it.

import { Isa } from 'isa-sdk';

const isa = await Isa.withBearer();
await isa.zyins.datasets.get();

// Exact term resolves to a known Concept.
const known = isa.zyins.conditions.match('high blood pressure');
console.log('known.isKnown:', known.isKnown);
console.log('known.kind:', known.kind);
console.log('known has id:', known.id !== null);
console.log('known has name:', known.name.length > 0);

// Unknown term returns isKnown:false — it does NOT throw.
const unknown = isa.zyins.conditions.match('zzzznotacondition');
console.log('unknown.isKnown:', unknown.isKnown);
console.log('unknown.kind:', unknown.kind);
console.log('unknown.id is null:', unknown.id === null);
console.log('unknown preserves input:', unknown.inputText === 'zzzznotacondition');

Expected

known.isKnown: true
known.kind: condition
known has id: true
known has name: true
unknown.isKnown: false
unknown.kind: unknown
unknown.id is null: true
unknown preserves input: true

The medication accessor works the same way:
isa.zyins.medications.match('lisinopril') returns a MedicationConcept.


6. Autocorrect — a typo is corrected

Setup — the bundle must be loaded; the default autocorrector is wired to
the dataset's spelling table.

Steps — create 06-autocorrect.ts and run it.

import { Isa } from 'isa-sdk';

const isa = await Isa.withBearer();
await isa.zyins.datasets.get();

const fixed = isa.zyins.autocorrector.correct('hyprtension', { mode: 'submit' });
console.log('corrected:', fixed);
console.log('typo was changed:', fixed !== 'HYPRTENSION');

Expected — the misspelling is rewritten to the canonical term
(uppercased; the autocorrector normalizes to uppercase tokens).

corrected: HYPERTENSION
typo was changed: true

7. Autocomplete — a prefix returns ranked suggestions

Setup — the bundle must be loaded. autocomplete() returns a ranked
Suggestion[], best match first (rank: 0).

Steps — create 07-autocomplete.ts and run it.

import { Isa } from 'isa-sdk';

const isa = await Isa.withBearer();
await isa.zyins.datasets.get();

const suggestions = await isa.zyins.medications.autocomplete('lisi', { limit: 5 });
console.log('returned suggestions:', suggestions.length > 0);
console.log('top rank is 0:', suggestions[0]?.rank === 0);
console.log('top suggestion:', suggestions[0]?.name);
for (const s of suggestions) console.log(s.rank, s.name, s.score);

Expected — a non-empty list, ranked from 0, with lisi-prefixed
medications at the top.

returned suggestions: true
top rank is 0: true
top suggestion: LISINOPRIL     // varies
0 LISINOPRIL                4121   // varies
1 LISDEXAMFETAMINE          2241   // varies

7a. Typo-tolerant autocomplete (requires the typo-tolerance release)

Version gate. This sub-step requires isa-sdk at or above the
typo-tolerance release. On an earlier SDK, chrons returns no Crohn's
suggestion — that is expected on the old version, not a failure. Record
the installed version (npm ls isa-sdk) with your result.

import { Isa } from 'isa-sdk';

const isa = await Isa.withBearer();
await isa.zyins.datasets.get();

const exact = await isa.zyins.conditions.autocomplete('crohns', { limit: 5 });
const typo = await isa.zyins.conditions.autocomplete('chrons', { limit: 5 });
console.log('crohns top:', exact[0]?.name);  // expect a Crohn's-family condition
console.log('chrons top:', typo[0]?.name);    // expect the SAME, on the typo-tolerance release

Expected (typo-tolerance release) — both queries surface the same
Crohn's-family condition at the top.


8. Fuzzy match (opt-in) — recovers misspellings

Setup — fuzzy matching is opt-in. Construct the client with a
FuzzyMatchAlgorithm; it replaces the default exact matcher on every
match() call. The default matcher is unchanged for clients that do not
opt in.

Steps — create 08-fuzzy.ts and run it.

import { Isa, FuzzyMatchAlgorithm } from 'isa-sdk';

const isa = await Isa.withBearer(undefined, undefined, {
  matchAlgorithm: new FuzzyMatchAlgorithm(),
});
await isa.zyins.datasets.get();

// 'corhns' is a transposition of 'crohns'; 'sertaline' drops a letter
// from 'sertraline'. The default matcher misses both; fuzzy recovers them.
const cond = isa.zyins.conditions.match('corhns');
const med = isa.zyins.medications.match('sertaline');
console.log('corhns resolved:', cond.isKnown, cond.name);
console.log('sertaline resolved:', med.isKnown, med.name);

Expected — both misspellings resolve to a known Concept.

corhns resolved: true CROHN'S DISEASE     // varies in display form
sertaline resolved: true SERTRALINE        // varies in display form

9. Errors — a malformed request throws a typed error

Setup — the authenticated client. The SDK throws a typed error
hierarchy; QA confirms instanceof matching works and the error carries a
stable machine-readable code.

Steps — create 09-errors.ts and run it. The request below is
deliberately malformed (impossible state code) so the engine rejects it.

import { Isa, IsaApiError, IsaValidationError, Sex, Height, Weight, NicotineDuration, Coverage, ProductSelection, Products } from 'isa-sdk';

const isa = await Isa.withBearer();

try {
  await isa.zyins.prequalify({
    applicant: {
      dob: '1962-04-18',
      sex: Sex.Male,
      height: Height.fromFeetInches(5, 10),
      weight: Weight.fromPounds(195),
      state: 'ZZ', // not a real US state — the engine rejects this
      nicotineUse: { lastUsed: NicotineDuration.Never },
    },
    coverage: Coverage.faceValue(25_000),
    products: ProductSelection.of([Products.Fex.AetnaAccendo]),
  });
  console.log('UNEXPECTED: no error thrown — investigate');
} catch (err) {
  console.log('is IsaValidationError:', err instanceof IsaValidationError);
  console.log('is IsaApiError:', err instanceof IsaApiError);
  if (err instanceof IsaApiError) {
    console.log('stable code:', err.code);
    console.log('request id present:', typeof err.requestId === 'string');
  }
}

Expected — a typed error is thrown (a malformed request must not return
a result). IsaValidationError is a subclass of IsaApiError, so both
instanceof checks are true, and the error carries a stable code.

is IsaValidationError: true
is IsaApiError: true
stable code: validation_error
request id present: true

Match your branching on err.code (stable enum), never on the human
message text (which may change wording).


10. Idempotency — same key replays, different body conflicts

Setup — the SDK auto-mints a fresh UUID v4 Idempotency-Key per call,
so SDK-level replays are automatic and invisible. To observe the replay and
conflict behavior directly, exercise the wire with curl, where you set the
key yourself.

The deduplication window is 24 hours, keyed by
(account, Idempotency-Key, request-body-hash).

Steps

The two curl calls below share one literal Idempotency-Key
(550e8400-e29b-41d4-a716-446655440000) so you control the replay. Use a
fresh UUID v4 of your own each time you re-run the whole step.

  1. Send the request with the shared key. Save the request_id from the
    response body.
curl -X POST https://zyins.isaapi.com/v3/prequalify \
  -H "Authorization: Bearer $ISA_TOKEN" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"applicant":{"sex":"male","dob":"1962-04-18","height_inches":70,"weight_lbs":195},"coverage":{"face_amount_cents":2500000,"state":"NC"},"products":["prod_c4f6d9a8-2b1e-5d7c-8e3a-9f0b1c2d3e4f"]}'
  1. Re-run the identical curl above (same key, same body). Compare the
    request_id to step 1 — they must match.

  2. Reuse the same key with a changed body (a different
    face_amount_cents) to force a conflict.

curl -X POST https://zyins.isaapi.com/v3/prequalify \
  -H "Authorization: Bearer $ISA_TOKEN" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -H "Content-Type: application/json" \
  -d '{"applicant":{"sex":"male","dob":"1962-04-18","height_inches":70,"weight_lbs":195},"coverage":{"face_amount_cents":1000000,"state":"NC"},"products":["prod_c4f6d9a8-2b1e-5d7c-8e3a-9f0b1c2d3e4f"]}'

Expected

  • Step 1 and step 2 (same key, same body): both return 200. The
    response envelope echoes the idempotency_key verbatim, and the
    request_id is identical across both calls — the server replayed the
    cached response instead of re-evaluating.
  • Step 3 (same key, different body): 409 Conflict. The server refuses
    to replay a cached response for a different request. Mint a fresh key for
    the changed request.

The envelope-echoed idempotency_key is byte-identical to the
Idempotency-Key header you sent. If they ever differ, that is a protocol
bug — file a ticket quoting both.


How to report a failure

When any Expected block does not match, capture these three things so an
engineer can reproduce without guesswork:

  1. request_id — from the response envelope (data calls expose it as
    envelope.requestId; errors expose it as err.requestId; curl returns
    it in the JSON body as request_id). This correlates your call to the
    server logs.
  2. The exact input — the full request you sent (applicant, coverage,
    products, or the curl body), plus which step number failed.
  3. The SDK version — run npm ls isa-sdk and paste the version. Several
    steps (the typo-tolerance autocomplete in 7a) behave differently across
    releases.

Open the ticket against the SDK repo with the step number in the title.


Did this page help you?