Issue Tracking #
Issue Tracking is an issue tracker with no user interface, operated by Turn Zero Cloud. People, agents, and programs file reports of what does not work or what is missing, rate their experience, and work each report through to an outcome. Every action is a call made from the caller's own tools. The package states the service's contract and includes a client and a test double for an application's backend and its tests.
Use it when #
Use Issue Tracking when an application needs one queue for bug reports, feedback, the checks its test runs record, and ratings that agents file and read as readily as people do. Declare the issue_tracking service in the manifest, and the platform creates one space, the set of records one owner keeps, shared by both of the application's environments. Your backend reaches the space through the platform's egress gateway, which presents the space's credential for it.
Use it for an application's own end users: the backend files each user's report under the user's identity and reads the queue through the client. When your backend files a report for one of your users, send that user only their own report's state. Do not send the call's whole answer: the issue in it holds other reporters' words. Your tool reads the whole space, files into it, rates in it, and settles its reports through the platform's feedback actions, naming the space (Feedback in your application's space).
Do not use it for work that needs boards or sprints, for notifications, or for fields your application defines. The service sends nothing, and an issue moves only through the calls and the passes, along one fixed set of allowed moves between its states. The passes are the AI runs that match new reports to known issues (What the triage passes do). The passes run on Turn Zero's own AI allowance, never on your application's. They read a space only where its configuration turns them on, through the contract's configure call (Configure the space).
What it provides #
- Spaces. A space contains one owner's records and is identified by a universally unique identifier (UUID). Each of its tokens has one of four permission levels, from lowest to highest
file,work,record, andoperator, one token per holder. Every call made with a token reads and writes that space alone. Keep afiletoken in your backend and give it to no end user, device, or client: its holder names the reporter each call is for. - Attributed writes. Every write names the person, agent, or program that acted. A service that relays its users' actions names each user, never itself, and a relayed read returns that user's own reports and nothing of anyone else's.
- Issues and reports. An issue is the one curated record of a problem. It has a short identifier such as
#12, a kind, a state and an outcome, a priority, a component, what it waits for and who claims it, and the counts of its reports. A report is one filing in the filer's words, never edited, linked to the issue with the reason for the link. - The kinds.
bug,gap,docs,friction,question,task,praise, andidea;problem,error, andmistakefile abug. Anideatakes no priority and is never fixed, andnextnever hands it out. Later requests link to it, so its report count is its demand. - What an issue waits for.
waiting_fornames who or what it waits on: null,person(the space's owner),person:<account>(one member of a shared space),reporter,outside:<what>, orevidence, which stands exactly while the issue iswaiting. An issue waiting for a person, the reporter, or an outside event stays open, and nothing closes it on its own.listandcountfilter and group by it and by the claim,noneselecting the issues with no value. - Who claims it.
claimed_bynames who works the issue: null, or a label of 1 to 200 printable characters.nextalone writesbranch:<name>with its lease; a session, an agent, or another worker claims throughupdateunder any other label. A claim while another stands is refusedlease_conflict, andgive_backwith a reason clears it: a label claim by any caller at theworklevel, a branch's lease only with its secret. A claim untouched for a day reads as stale. - Matching at filing. The service fingerprints the evidence the platform stamped on a report and links a repeat to the known issue without any AI. A report whose origin is not
workspaceorplatformis never linked by its evidence onto a restricted issue, one marked as a security concern that the service hides from other reporters. A follow-up under the filer's own problem key, the filer's own name for the problem, is a new report on the same issue. Where the issue waits on its reporter, itswaiting_forreadingreporter, its reporter's follow-up clears that and returns it to the queue. - What a fingerprint needs. A report links by its fingerprint only where its evidence names the call's
refusal. Two reports of a call that was answered, naming no refusal, are never linked by it. - Undoing a confirmation. Where a filer's
followof a likely match closes the issue its filing opened, which holds that report alone, as a duplicate, anunmergeof that issue also withdraws the link thefollowmade. The report then stands on its own issue alone. - A repeat of a declined request. A filing's
candidates, its likely matches, include an issue closeddeclinedoras_designed: a request considered and not taken. It ranks after an open issue that matches as well, and a closed restricted one is never offered, whatever the filing's origin. Each candidate carries itsoutcome, null while the issue is not closed, and a fixedsentence, such as "Not taken.", and never the resolution's words. Afollowonto such an issue adds the report and leaves the issue closed. - Why a candidate matched. Each candidate names its issue's
kindandcomponent, and itssummarywhere a trusted actor wrote one. Itsmatched_evidencenames the members of the filing's evidence that equal those of the standing reports whose evidence chose the issue, and is empty where only the title's words matched. - Clearing a resolution. On the service's current calls, an
updatenamingresolution: nullclears the resolution a new or open issue carries, and thatupdatewrites none.settleandwaitwrite one. TheupdatethatcreateIssueTrackingClientmakes still writes a resolution, on any state. - Amending your own report. A filer corrects or withdraws a report it filed with
submitnamingamends, the report's number, never the issue's. The amendment is a new report that hasamendsandwithdraws. It is linkedvia: amendmentto each issue the first report belongs to at that moment. So the triager reads the two side by side, and the first report stays as filed. Naming another actor's report is refusedreport_not_found, as is a number that matches no report. - What an amendment names. It names at least one of
title,text,impact,workaround,proposed_resolution,attachments, andwithdraws. Its kind and problem key are the first report's, and so are its title and impact where it names none. A withdrawal moves nothing by itself: a person reads it and settles the issue.createIssueTrackingClienthas noamends, so a backend amends through the gateway withcreateIssueTrackingClientV2(Methods and results). - Images on reports and comments. A filing and a comment can each carry up to four images, PNG, JPEG, or WebP, each at most 512 KiB, judged by their bytes and never by a name. The service stores each beside its report, under the report's own privacy and restriction, within 16 MiB a space. A read lists each image as
{ id, type, bytes, sha256 }, andGET /v2/attachment/<id>under aworktoken fetches it. A retry stores no image, an operator'sredactremoves one whole, the retention drop takes a report's images with its text, and no AI pass reads one. - A note to reporters. An issue can carry a note of up to 2,000 characters for all its reporters, which a read naming a reporter returns. An
updateunder aworktoken or above writes or clears it on any state, and no filing sets it. Never quote a report in it, or name a reporter or a restricted issue. Afixesorresolveslanding writes a note naming its commit and when it landed, which a reporter reads only while the issue is fixed or closeddoneorverified. Through the gateway, onlycreateIssueTrackingClientV2's reads return it. - Praise apart. Praise and every other kind are kept on separate issues. A report filed as
praiseopens or joins a praise issue, and a report of another kind joins no praise issue, even under a problem key once used for praise. Where a praise report and a problem's issue meet, the service relates the two issues, unless either is restricted, and links neither report across. No merge joins a praise issue with an issue of another kind, and a praise report never reopens a fixed issue. - Mentions become relations. On the service's current calls, a comment, or an
updatethat sets the summary, relates its issuerelates_toto each other issue its text names, such as#12. The service makes each relation itself, at most five a write, and the answer'smentionedlists them. It skips the issue itself, a number with no issue, a pair already related or unlinked, and a restricted issue beside one that is not. A quoted line opening with>links nothing, nor does a number inside a word, a path, or an entity. A report's text links nothing. - Who waited on a settled issue. On the service's current calls,
settleanswersrelated_open: up to 20 entries, one for eachblocks,part_of, orrelates_torelation to an issue not closed, in either direction. Each entry names the relation'stype,from, andtarget, and the other issue'sstate.blockscomes first, thenpart_of, thenrelates_to, each by level and then by number. A restricted issue is listed only when the settled issue is restricted too. Where 20 are listed,readthe issue for the rest. - Builds and checks. A build records a version of a release line, the sequence of versions an environment runs, written by the deploy. A check records each run of a monitored job under a stable key, and an issue opens from it only once the failures repeat. An issue a check opens reads
blockedwhere the check breaks after at least three green runs and the failure is in the product or the client. A build that names no line counts toward neither. A landing records the commit that fixed an issue, with its regression test and proof. - Withdrawing a run. The job that posted a check's run can withdraw it by the key it posted it under. The withdrawal stays in the check's history and closes an episode the run opened. A
keygiven with it is the withdrawal's own retry key, never the run's. - Verification by exposure. A fixed issue closes verified once live builds that contain the fix have exercised it a set number of times, with no report of the same point since. An issue nobody can explain waits, and the next report reopens it. A bug or a friction closed
doneby hand, with no landing, names infixed_inthe build that serves the fix where a returning reporter filed on it, and may name it otherwise. A later report then reopens the issue only when it comes from that build or a later one, and with nofixed_init reopens nothing. - Passes. Two AI passes propose and check duplicates, splits, and themes, and the triage pass judges each new issue to give it a priority from 1 to 3. A merge is applied where they agree and the evidence on at least one side was stamped by the platform or confirmed against its records. A pass never edits a report, and never moves a priority a person or a program wrote. How an issue gets its priority explains each rule.
- What holds a merge. A merge the passes agree on waits while its duplicate carries a
plan, isfixed, or carries a branch's claim. Once the duplicate is free, the passes apply the merge at their next write, or withdraw it where it no longer fits their limits. An agreement lapses where the master closes, a report joins either issue, or a component is set. A merge withdrawn because its master closed as a duplicate is proposed again against the surviving master. A person'ssettleapplies a merge held by a plan at once, and so doescreateIssueTrackingClient'ssettlewith the actapprove. - What the passes propose. A
readwithoutactoranswersproposals: each merge, split, or theme the passes proposed that still waits and reaches the issue. A merge reaches its duplicate and its master, a theme its issue, and a split the first issue its report is linked to. Each entry carries itsid, itstype, its other end, the passes'verdicts, itsdisagreements, its instantat, andrestricted.listwithproposed: trueselects the issues a waiting proposal reaches. - How often the passes' merges were wrong.
read_spacereturnssampled_merges,{ rate, audits }: the share of the last thirty days' audited merges judgedwrong, 0 where none was audited, and how many verdicts it counts. Whilerateis above the configuration'spropose_only_threshold, an agreed merge waits for a person.relay_issue_actforwardsread_spaceonly under anownertoken. - Ratings and asks. The human series holds the standard Net Promoter Score question, scored 0 to 10, and the agent series an agent's rating of how difficult its task was, scored 1 to 5. The two are never combined. An ask records a request for a person's rating, and a throttle limits how often one account is asked.
- A space's figures.
countreturns how many issues have the state, outcome, kind, component, label,waiting_for,claimed_by, level, or requirement you name, leaving out issues with labels you name. It returns a total, or totals by any one field except the requirement.ratesreturns how many issues were filed, closed by outcome, reopened after a fix, and still open, per day or week over up to 400 steps. It also returns the time to close by kind and level. Both take anoperatortoken.relay_issue_actforwards both under anownertoken, and the gateway forwards neither (Keep your own issue list). - Counting through the gateway. A deployment counts the issues its
listanswers. It pageslistwithlimit: 100until the answeredcursoris null, since a page can hold fewer issues than its limit while more follow. A large space is counted over several minutes, its pages paced within the space's rate. From development alistnames its actor, so that count holds the actor's own issues alone. - Reads and erasure. List, search, and export read a space. Erasure replaces a person's identity on every record and keeps every report's text, so leave out of a report anything you do not want kept.
- A client and a test double, which The client and Test with the double describe.
States, permission levels, and moves #
An issue is in one of five states: new, open, waiting, fixed, and closed. A filing opens it new where the space's passes run, and open where they do not, the default. Triage, or an update that sets its kind or component, moves a new issue to open.
wait parks a new or open issue as waiting, with what would settle it, as a green check does for the open issue it opened. With waiting_for set to person, person:<account>, reporter, or outside:<what>, it sets what the issue waits for and moves no state. An update giving a waiting issue any other value is refused state_conflict, reopen the way out.
A fixes or resolves landing recorded with land moves an issue to fixed or closes it done. A narrows landing leaves its state, and a revert reopens it. An operator's settle closes an issue with its outcome, and reopen moves a fixed, closed, or waiting issue back to open. A new report, or one a merge moves in, reopens a waiting issue or one closed unexplained.
A closed issue carries one outcome: verified, done, as_designed, declined, duplicate, superseded, noted, or unexplained. A fixed issue closes verified once live builds have exercised the fix enough times with no report of the same point, and one nobody can explain closes unexplained after the long wait. A praise filing that opens its issue closes it noted at once.
Each token has one permission level, and a level allows its own calls and those of every level below it:
filefiles withsubmitand confirms a repeat withfollow, for the reporter each call names.workreads, lists, and searches issues, sets an issue's curated members withupdate, comments, relates, and splits. It sets what an issue waits for withwait, takes work withnext, gives work or a label claim back, and records ratings and asks.recordconfirms a report's evidence and records landings, builds, shipments, exposures, and checks.operatorsettles, reopens, and unmerges issues, clearsrestrictedmarks, redacts, reads counts and rates, exports, erases an actor, reads and restores the record, configures the space, mints and revokes tokens, and deletes the space.
Restricted issues #
The service hides a restricted issue from a reporter and from no one else. Below the work permission level, and in every read that names a reading actor, a restricted issue is never returned. From the work level up, every read returns it as it returns any other issue. A filing whose origin is not workspace or platform is offered no restricted issue as a likely match, under any token. The one exception is a filing under an operator token that names its caller's relayed_level as work or above, as the platform's relay does at the contribute level.
A filing marked restricted links only to restricted issues, and otherwise opens its own restricted issue. A later confirmation of its report links it only to restricted issues too. A filing kept back from a restricted issue opens its own issue restricted as well, and so does a report a later confirmation keeps back, where its issue holds that report alone.
Where a report is kept from a restricted issue closed done or verified, the daily sweep surfaces that issue once with the label recheck and leaves it closed. A person reads it and reopens it or leaves it.
Code that reaches the service with its own token reads these answers:
- Under a
workorrecordtoken,list,search, and the counts ofread_spaceinclude restricted issues. - A
listthat namesactoranswers no restricted issue, whatever the token, as the reporter's own view never does. Withproposed: trueit selects no issue by a proposal that reaches a restricted issue. - A
read'sproposalsentry that reaches a restricted issue carriesrestricted: true. The calls that answer it take aworktoken, so a reporter never reads one. - On the calls
createIssueTrackingClientmakes, an issue record carriesrestricted,trueorfalse. Theirupdatesets norestricted, and is refusedinvalid_requestwhere it names one. - On the service's current calls, an
updatethat setsrestrictedtofalseis refusedgrant_requiredunder a token belowoperator, before the key is read. Setting it totrueneeds aworktoken.
The restricted mark follows the words. When one issue is merged into another, the issue kept is restricted where either was, and its merged entry carries restricted: false where the merge set the mark. A split from a restricted issue opens restricted, a person's as well as a pass's. An unmerge leaves each issue's mark as it stands.
The triage pass looks for a restricted issue's likely matches, and offers a restricted issue among another issue's. A merge or a theme between a restricted issue and one that is not waits for a person, and the passes withdraw it where it no longer fits their limits. A merge or a theme of two restricted issues follows the usual limits.
The second reader is the passes' second reading of an issue's priority and of a closed issue's restricted mark. It reads a restricted issue closed declined or as_designed after its other readings in the critic's run. Where both its readings find no exposure, it sets restricted to false in one updated entry. Otherwise it writes one kept entry, a history entry recording that it kept the mark, and the mark stays. After a person redacts what it found, it reads the issue again. The calls createIssueTrackingClient makes return no kept entry.
When a pass gives up on an item, it writes a critic_failed entry on the item's issue. The entry names the item and why: three failed attempts, or 36 hourly runs with no answer from the model. An item given up on for want of an answer is still tried once each run, and the first answer written for it adds a critic_cleared entry. The calls createIssueTrackingClient makes return neither entry.
How the gateway keeps both environments apart #
The space is created when the manifest is first submitted. Your deployments in each environment file into it and read from it, and so do your local runs, under the development credential. The submission result's issue_tracking list gives the space, and never a token. An application that has a space for each environment keeps both until it is deleted, its production space created at its first promote, or its first production deploy where it has one environment.
The gateway keeps the two environments apart in three ways:
- It stamps every report your backend files with the environment the call came from and the version it serves,
localfrom your own machine. On the first client'ssubmitthese areevidence.environmentandevidence.version. OncreateIssueTrackingClientV2's they are oneseen_inentry,{ line: null, version, environment }, in place of any the call names, and an amendment carries none. - It puts
development:in front of every person, agent, or program id a development call names, so a development tester never appears as one of your production users. From development, alist,list_signals, orexportwithoutactoris refusedinvalid_request, as arereadandsearchon either client. So is a development write that names another issue by its id, and an id that already starts withdevelopment:or passes 200 characters once prefixed. - A deployment reaches the space at its binding's grant level:
reportfor the application's own space or a space for each environment, andreportorcontributefor a space of your account. Atreportit files reports and reads the space, and the gateway refusesissue_tracking_call_refusedevery call that changes an issue:update,comment,relate,settle,signal,export,erase_actor,open_ask, andclose_ask.
Every filing on createIssueTrackingClientV2 reaches the space with the origin field, whatever it names, so the service reads it as a reporter's. One naming an evidence_basis other than claimed is refused invalid_request, and so is one naming relayed_level.
At report, the gateway leaves a restricted issue out of every response, whichever space the binding names. A list or a search skips it, and a read of it is refused as for an issue the space does not hold. The response to a filing or an amendment names no restricted issue. A read's proposals leave out each entry that reaches one, and a reporter's own view never holds one. The counts read_space returns include restricted issues, because the gateway forwards a count as it comes.
At report a list that names proposed is refused issue_tracking_call_refused on either client; its other filters still work. A read at report leaves out each history entry that records a proposal. On the first client's calls there, every issue's proposed reads false, and a read leaves out a relation that is still only proposed.
A finding is an issue that groups other issues, its members, stored as a task labelled finding. On the first client's calls at report, a finding's members list only the issues that are in it. A finding whose own proposed field is true is skipped as a restricted issue is, as is one a proposal made and nobody approved.
The first client's list and search also skip a finding the passes made that a person approved and later declined by hand, though a read of it still returns it. On createIssueTrackingClientV2 the gateway skips every finding with no report of its own, approved ones included, so a backend at report sees no such finding. Read findings at a higher grant level.
The contribute grant level adds the working calls, and owner reaches every call; no deployment has owner. The call that starts a deploy may name commit, 7 to 64 lower-case hexadecimal characters. After each deploy, promote, and rollback, the platform writes a build record into the application's own space naming the version, the environment, and that commit, never delaying the deploy. A malformed commit, or one on the call that only prepares an upload, is refused invalid_request. An application with a space for each environment gets no build record.
A space of your account's own #
A manifest can instead name a space your account created with create_issue_space, and the platform binds the application to it under the same three rules. Bind an application to the space gives the space and level to name, the release line the platform adds, and their refusals.
Deletion and copies #
The platform keeps the space's token in custody under a platform-minted name, and no application and no author has it. A manifest revision that drops the kind changes nothing: the space and its records remain. The platform reads a space's records only through the feedback actions naming the space, the export, the deletion of your account, and the daily copy.
Deleting the application deletes its own space with every record in it, and ends a binding to a space of your account's own. Deleting your account deletes every space of its applications and its own. Deleting the development environment deletes nothing of a shared space, ends a development binding, and deletes a per-environment development space. A deleted space cannot be recovered.
Once a day, the platform copies each space your account holds and each application's own space into storage it holds for your account. A copy is kept 35 days, the newest until a newer one is written, and a space's copies are deleted with it. Per-environment spaces are not copied, and no action reads or restores a copy.
The daily copy lists a report's or a comment's images and carries none of their bytes; a copy the contract's read_record answers carries them, and a restore from it brings a redacted image back. Deleting a space deletes its images with it, and an operator removes one image whole with redact.
Availability #
Version 0.31.0 is the package's current version, and this page describes it. Until the library's next publish, list_library and read_library_entry can return an earlier version, the one you can take, so this page can describe a change that version lacks. The entry includes the testing subpath from version 0.29.0. Under the package's earlier name, issue_tracking, the subpath stood from version 0.1.0. The entry contains the compiled clients and double as the npm package @turnzero/tztrack, the package's product statement and detailed contract, and its integration guide. The contract's "Migration to" sections list what changed at each version that moved a promise.
Turn Zero Cloud runs the operated service and creates one space for every application that declares the kind. The calls, the records, and the spaces' stored bytes count against no plan quantity, and no charge attaches to a space, a call, or a record (Plan and usage). The service limits each space's forwarded calls per minute, and a call past the limit is refused rate_limited. When many reads of large spaces run at once, the service can also refuse a read rate_limited: wait a few seconds and call again.
Related feature packages #
The tztrack package depends on no other feature package. Storage states the rule this service applies to every write: where a service relays its users' actions, each write names the user who acted. Account verifies the end users an application names as actors.
The client #
The package is published in the library as the npm package @turnzero/tztrack, not on the npm registry; Use the library installs it with one line. The module holds two clients: createIssueTrackingClient, the one a hosted backend uses for most calls, and createIssueTrackingClientV2 for the service's current calls. The gateway forwards four of the current calls, submit, read, list, and search, so your backend can amend its users' reports. A relay of your account reaches the rest.
Construction #
createIssueTrackingClient(transport) builds the hosted backend's client, with no options. transport(path, init) sends each call to POST /egress/v0/issue-tracking/v1/<call> under the application's platform credential, TURNZERO_CLOUD_APP_TOKEN, and returns the response. The path sits on TURNZERO_CLOUD_GATEWAY_URL where the deploy sets it, and on TURNZERO_CLOUD_API where it does not. The gateway finds the space from the calling application and environment, and presents the space's token for it. A local run reaches the same space through the platform's public gateway under the development credential, and its reports are stamped local. A test runs the double in its place (Test with the double).
createIssueTrackingClientV2(transport), also with no options, runs over the same transport, each call at /egress/v0/issue-tracking/v2/<call>.
Methods and results #
The gateway forwards submit, read, list, search, read_space, list_signals, and read_ask at the report grant level, each a method of the client, such as client.submit(request) and client.listSignals(). Each method takes { signal } as its last argument.
submit({ actor, kind, source, title, body, evidence, key })files one report,actorbeing{ kind, id }for the user you file for. It returns{ issue, outcome }, the outcome one offiled,retried,repeated,reopened, andrecovered.read(issue)returns the issue, its history, comments, relations, and members.listandsearchreturn issues,listin the ordernewestorscore, the service's rank by an issue's reports.- On this client, a priority is 1 to 4.
excerptmay only befalse, andtrueis refusedinvalid_request.recovered: trueis refusedrecovery_unmatchedunless that actor's own filing under that key opened the issue and every report on the issue is that actor's.
On createIssueTrackingClientV2 the gateway forwards submit, read, list, and search at both grant levels, and refuses every other call issue_tracking_call_refused. Make those calls with the first client.
submit({ actor, origin, kind, title, text })files one report, and returns itsreport, whoseidis its number, with theissueand theoutcome. The gateway files it with the originfield, and the service refuses aquestionor ataskfrom that origin, so file those with the first client.submit({ actor, amends, text, withdraws })corrects or withdraws a report thatactorfiled,amendsbeing its number. It returns the outcomeamended.read({ issue, actor }),list({ actor }), andsearch({ query, actor })return that user's own view: the issues their reports belong to, each with their reports and each report'samendsandwithdraws.
Failures #
IssueTrackingGatewayRefusal, for the gateway's own refusal, which the client tells apart by the marker header.issue_tracking_call_refusedis a call the gateway does not forward, andissue_tracking_application_requireda credential that names no application.issue_tracking_not_provisionedis an environment with no space, andissue_tracking_unavailablea token that cannot be read, which may be retried.IssueTrackingRefusal, for the service's refusal by name inrefusal, withstatusanddetail. A refused call changes nothing. Itsownis null except on areadnamingactorrefusedissue_not_found, where that user's own report joined the issue the number names. Thereownis the identifier of the issue that report is on now. A read naming the same actor returns that issue.ownnever names an issue that user cannot read. It is null for another user's issue, for a restricted issue, and for a number that names no issue.IssueTrackingUnreachable, where the transport failed or the service answered a fault of its own. Retry with the samekey.
Branch on refusal, never on detail. The refusals page lists the gateway's four refusals with their cause and remedy. The service's own names, recovery_unmatched among them, are listed in the package's contract, the entry's tztrack.md.
Test with the double #
createIssueTrackingDouble(options) from @turnzero/tztrack/testing answers both clients' calls in memory, and passes the same list of test cases the contract states. Seed a space with double.createSpace(), then take double.gatewayView({ space }) over its space.id: the gateway's view, as the deployed backend reaches its space. Build createIssueTrackingClient(view.transport) with no options, and createIssueTrackingClientV2(view.transport) for the four current calls the view forwards. reset() empties every space, so seed a space and take its view in each test. A test runs this sample over the double.
import assert from 'node:assert/strict';
import { createIssueTrackingClient, createIssueTrackingClientV2, IssueTrackingGatewayRefusal } from '@turnzero/tztrack';
import { createIssueTrackingDouble } from '@turnzero/tztrack/testing';
// One space, reached as the deployed backend reaches it: through the gateway's view.
const tracker = createIssueTrackingDouble();
const { space } = tracker.createSpace();
const view = tracker.gatewayView({ space: space.id });
const client = createIssueTrackingClient(view.transport);
// The backend files the signed-in user's report under the user's own id.
const filed = await client.submit({ actor: { kind: 'person', id: 'user-1' }, kind: 'bug', source: 'person', title: 'Export does nothing', body: 'Clicking Export downloads no file.' });
assert.equal(filed.outcome, 'filed');
// Answer the user with their own report's state alone, never the whole issue.
const reply = { issue: filed.issue.id, status: filed.issue.status };
assert.equal(reply.status, 'open');
// The same transport carries the current client's filing and the user's own amendment.
const current = createIssueTrackingClientV2(view.transport);
const first = await current.submit({ actor: { kind: 'person', id: 'user-1' }, origin: 'field', kind: 'bug', title: 'Import fails' });
const amended = await current.submit({ actor: { kind: 'person', id: 'user-1' }, amends: first.report.id, withdraws: true });
assert.equal(amended.outcome, 'amended');
// A call the gateway refuses throws its own refusal, told apart from the service's.
view.refuseNext('issue_tracking_unavailable');
await assert.rejects(client.list(), (error) => error instanceof IssueTrackingGatewayRefusal && error.refusal === 'issue_tracking_unavailable');
The client's refusals are those Failures lists. These come from the double's own setup:
| Refusal | Cause | Remedy |
|---|---|---|
issue_tracking_call_refused, issue_tracking_application_required, issue_tracking_not_provisioned, issue_tracking_unavailable |
view.refuseNext scripted the gateway's refusal, or the call named a path the gateway does not forward, such as delete_space. |
Call through the client, and assert the refusal where the test scripts it. |
issue_tracking_not_provisioned, unscripted |
The view's space does not exist: it was never seeded, or reset() emptied it. The view returns this refusal on its own for every call to a space that does not exist. |
Seed the space with double.createSpace() and take its view again in each test. |