[{"data":1,"prerenderedAt":1717},["ShallowReactive",2],{"docs-sections":3,"docs-page-\u002Fdocs\u002Ftesting":123},[4,9,13,17,22,26,30,34,38,42,46,50,55,59,63,67,72,76,80,85,89,93,97,102,106,110,114,119],{"path":5,"title":6,"description":7,"group":8},"\u002Fdocs","Introduction","What tunables is, the problem it solves, and the principles it holds to.","Get started",{"path":10,"title":11,"description":12,"group":8},"\u002Fdocs\u002Fquickstart","Quickstart","Take one group of settings from constants to a screen with a history, in a Nuxt app on Postgres.",{"path":14,"title":15,"description":16,"group":8},"\u002Fdocs\u002Fpatterns","What it is for","The kinds of value tunables keeps — live limits, plans, per-workspace and per-person values, model choices, values that bound each other, costly switches — and the feature that does each.",{"path":18,"title":19,"description":20,"group":21},"\u002Fdocs\u002Fsettings-and-groups","Settings and groups","Declare each setting once, in a group edited beside what it affects, and read it with types.","Concepts",{"path":23,"title":24,"description":25,"group":21},"\u002Fdocs\u002Fkinds-and-units","Kinds and units","The eight kinds of setting, the built-in units, your own units, and how values are checked and read.",{"path":27,"title":28,"description":29,"group":21},"\u002Fdocs\u002Fchecks-and-guards","Checks and guards","Rules across settings, guards for what your app keeps, within for presets, confirmations for costly changes, and notes.",{"path":31,"title":32,"description":33,"group":21},"\u002Fdocs\u002Fhistory-and-restore","History and restore","Every change is a revision with who, when, why and each value before and after; restoring one runs the same checks as any save.",{"path":35,"title":36,"description":37,"group":21},"\u002Fdocs\u002Fpresets","Presets","Named, reusable configurations with one default per slot, assigned by reference and bounded by the settings.",{"path":39,"title":40,"description":41,"group":21},"\u002Fdocs\u002Fscopes","Scopes","Values per workspace and per person, through layers — the system, a plan, the workspace, the person — each bounded by what it inherits.",{"path":43,"title":44,"description":45,"group":21},"\u002Fdocs\u002Fcatalogs","Catalogs","Lists of allowed choices, such as permitted models, whose entries are retired rather than deleted and cannot be retired while in use.",{"path":47,"title":48,"description":49,"group":21},"\u002Fdocs\u002Fdeployment-values","Deployment values","Values a deployment sets in its environment and nobody tunes while the app runs, declared once and listed in .env.example.",{"path":51,"title":52,"description":53,"group":54},"\u002Fdocs\u002Fstorage","Storage","Postgres through your own client, the four tables and how to create them, joining your transactions, and the storage contract behind it.","Integrate",{"path":56,"title":57,"description":58,"group":54},"\u002Fdocs\u002Fserver","Server and access","Mount tunables' API as a fetch handler in Nitro, Hono, Bun or Deno, as Express middleware or as an oRPC router, with access decided by your app.",{"path":60,"title":61,"description":62,"group":54},"\u002Fdocs\u002Faccess","Who can do what","End users bound by values they never see, staff who tune groups, owners and members who change only their own scope, and the terminal.",{"path":64,"title":65,"description":66,"group":54},"\u002Fdocs\u002Ftesting","Testing","Your app's tunables in memory for its own tests, with values set where a test needs them, and how to test rules, guards and confirmations.",{"path":68,"title":69,"description":70,"group":71},"\u002Fdocs\u002Fnuxt-layer","The Nuxt layer","Ready screens for a Nuxt UI app — a group's form, All settings, history, presets, assignments and catalogs — placed beside what they affect.","Screens",{"path":73,"title":74,"description":75,"group":71},"\u002Fdocs\u002Fvue-composables","Vue composables","Headless composables for building tunables' screens in any Vue app, on any design system.",{"path":77,"title":78,"description":79,"group":71},"\u002Fdocs\u002Ftheming","Theming","The screens bring no colors, fonts or CSS of their own; they take your Nuxt UI theme.",{"path":81,"title":82,"description":83,"group":84},"\u002Fdocs\u002Fcli","The CLI","The tunables command, tunables.config.ts, and which commands need a database.","CLI",{"path":86,"title":87,"description":88,"group":84},"\u002Fdocs\u002Fcli-check","Check and generate","The commands that need no database — checking definitions, writing .env.example, and keeping explanations and defaults in step with a pdd-kit Product Model.",{"path":90,"title":91,"description":92,"group":84},"\u002Fdocs\u002Fcli-values","Read and change values","The commands that read and change stored values, through the same checks and history as the screens.",{"path":94,"title":95,"description":96,"group":84},"\u002Fdocs\u002Fcli-skills","Install skills","Install tunables' agent skills for Claude Code, Codex CLI, Cursor, Gemini CLI and GitHub Copilot, and keep them current.",{"path":98,"title":99,"description":100,"group":101},"\u002Fdocs\u002Fskills","Agent skills","Three skills that let a coding agent adopt tunables, define settings the right way and verify that every value has one home.","Skills",{"path":103,"title":104,"description":105,"group":101},"\u002Fdocs\u002Fskill-adopt","tunables-adopt","Bring tunables into an app that has none yet, from an inventory of every value to a wired, synced app with the old homes removed.",{"path":107,"title":108,"description":109,"group":101},"\u002Fdocs\u002Fskill-define","tunables-define","Add, change, rename or remove one value in an app that uses tunables, with its home, its checks, its reads and what happens to stored values.",{"path":111,"title":112,"description":113,"group":101},"\u002Fdocs\u002Fskill-verify","tunables-verify","Audit an app's tunables against their rules, report every finding with where it is and the rule it breaks, and fix only the findings you choose.",{"path":115,"title":116,"description":117,"group":118},"\u002Fdocs\u002Fapi","API reference","Every export of the tunables package, by the subpath it is imported from.","Reference",{"path":120,"title":121,"description":122,"group":118},"\u002Fdocs\u002Ftasklane-example","Tasklane, the example app","A todo app with workspaces and people as tunables — two groups, plans as a layer, a catalog of models, scopes, a rule, a confirmation, and the playground that runs it.",{"id":124,"title":65,"body":125,"description":66,"extension":1712,"group":54,"meta":1713,"navigation":273,"order":1044,"path":64,"seo":1714,"stem":1715,"__hash__":1716},"docs\u002Fdocs\u002Ftesting.md",{"type":126,"value":127,"toc":1705},"minimark",[128,132,140,145,154,489,500,504,532,664,668,674,1339,1343,1361,1682,1686,1701],[129,130,65],"h1",{"id":131},"testing",[133,134,135,139],"p",{},[136,137,138],"code",{},"tunables\u002Ftesting"," gives your tests your app's own definitions over an\nin-memory store: no database, no setup beyond the definitions you already\nexport.",[141,142,144],"h2",{"id":143},"testtunables","testTunables",[133,146,147,149,150,153],{},[136,148,144],{}," takes the same options as ",[136,151,152],{},"createTunables",", without a store,\nand returns tunables already synced, so every group reads its defaults.",[155,156,161],"pre",{"className":157,"code":158,"language":159,"meta":160,"style":160},"language-ts shiki shiki-themes tunables-panel-light tunables-panel-light tunables-panel-dark","import { describe, expect, it } from 'vitest'\nimport { testTunables } from 'tunables\u002Ftesting'\nimport { catalogs, effects, groups, presets, scopes } from '..\u002Fserver\u002Fsettings'\n\nexport const definitions = { groups, presets, catalogs, effects, scopes }\n\ndescribe('limits', () => {\n  it('lets a new workspace open three projects', async () => {\n    const tunables = await testTunables(definitions)\n    expect((await tunables.get('limits', { scope: { workspace: 'new' } })).maxProjects).toBe(3)\n  })\n})\n","ts","",[136,162,163,207,227,268,275,313,318,347,373,398,477,483],{"__ignoreMap":160},[164,165,168,172,176,180,183,186,188,191,194,197,200,204],"span",{"class":166,"line":167},"line",1,[164,169,171],{"class":170},"sZ4mS","import",[164,173,175],{"class":174},"sp9ci"," {",[164,177,179],{"class":178},"sVz71"," describe",[164,181,182],{"class":174},",",[164,184,185],{"class":178}," expect",[164,187,182],{"class":174},[164,189,190],{"class":178}," it ",[164,192,193],{"class":174},"}",[164,195,196],{"class":170}," from",[164,198,199],{"class":174}," '",[164,201,203],{"class":202},"sjCzm","vitest",[164,205,206],{"class":174},"'\n",[164,208,210,212,214,217,219,221,223,225],{"class":166,"line":209},2,[164,211,171],{"class":170},[164,213,175],{"class":174},[164,215,216],{"class":178}," testTunables ",[164,218,193],{"class":174},[164,220,196],{"class":170},[164,222,199],{"class":174},[164,224,138],{"class":202},[164,226,206],{"class":174},[164,228,230,232,234,237,239,242,244,247,249,252,254,257,259,261,263,266],{"class":166,"line":229},3,[164,231,171],{"class":170},[164,233,175],{"class":174},[164,235,236],{"class":178}," catalogs",[164,238,182],{"class":174},[164,240,241],{"class":178}," effects",[164,243,182],{"class":174},[164,245,246],{"class":178}," groups",[164,248,182],{"class":174},[164,250,251],{"class":178}," presets",[164,253,182],{"class":174},[164,255,256],{"class":178}," scopes ",[164,258,193],{"class":174},[164,260,196],{"class":170},[164,262,199],{"class":174},[164,264,265],{"class":202},"..\u002Fserver\u002Fsettings",[164,267,206],{"class":174},[164,269,271],{"class":166,"line":270},4,[164,272,274],{"emptyLinePlaceholder":273},true,"\n",[164,276,278,281,284,287,290,292,294,296,298,300,302,304,306,308,310],{"class":166,"line":277},5,[164,279,280],{"class":170},"export",[164,282,283],{"class":170}," const",[164,285,286],{"class":178}," definitions ",[164,288,289],{"class":174},"=",[164,291,175],{"class":174},[164,293,246],{"class":178},[164,295,182],{"class":174},[164,297,251],{"class":178},[164,299,182],{"class":174},[164,301,236],{"class":178},[164,303,182],{"class":174},[164,305,241],{"class":178},[164,307,182],{"class":174},[164,309,256],{"class":178},[164,311,312],{"class":174},"}\n",[164,314,316],{"class":166,"line":315},6,[164,317,274],{"emptyLinePlaceholder":273},[164,319,321,325,328,331,334,336,338,341,344],{"class":166,"line":320},7,[164,322,324],{"class":323},"sOThN","describe",[164,326,327],{"class":174},"(",[164,329,330],{"class":174},"'",[164,332,333],{"class":202},"limits",[164,335,330],{"class":174},[164,337,182],{"class":174},[164,339,340],{"class":174}," ()",[164,342,343],{"class":170}," =>",[164,345,346],{"class":174}," {\n",[164,348,350,353,355,357,360,362,364,367,369,371],{"class":166,"line":349},8,[164,351,352],{"class":323},"  it",[164,354,327],{"class":174},[164,356,330],{"class":174},[164,358,359],{"class":202},"lets a new workspace open three projects",[164,361,330],{"class":174},[164,363,182],{"class":174},[164,365,366],{"class":170}," async",[164,368,340],{"class":174},[164,370,343],{"class":170},[164,372,346],{"class":174},[164,374,376,379,382,384,387,390,392,395],{"class":166,"line":375},9,[164,377,378],{"class":170},"    const",[164,380,381],{"class":178}," tunables ",[164,383,289],{"class":174},[164,385,386],{"class":170}," await",[164,388,389],{"class":323}," testTunables",[164,391,327],{"class":174},[164,393,394],{"class":178},"definitions",[164,396,397],{"class":174},")\n",[164,399,401,404,407,410,414,418,421,423,425,427,429,431,433,436,439,441,444,446,448,451,453,456,459,462,465,467,470,472,475],{"class":166,"line":400},10,[164,402,403],{"class":323},"    expect",[164,405,406],{"class":174},"((",[164,408,409],{"class":170},"await",[164,411,413],{"class":412},"sq0K3"," tunables",[164,415,417],{"class":416},"sBd0f",".",[164,419,420],{"class":323},"get",[164,422,327],{"class":174},[164,424,330],{"class":174},[164,426,333],{"class":202},[164,428,330],{"class":174},[164,430,182],{"class":174},[164,432,175],{"class":174},[164,434,435],{"class":178}," scope",[164,437,438],{"class":174},":",[164,440,175],{"class":174},[164,442,443],{"class":178}," workspace",[164,445,438],{"class":174},[164,447,199],{"class":174},[164,449,450],{"class":202},"new",[164,452,330],{"class":174},[164,454,455],{"class":174}," }",[164,457,458],{"class":174}," })).",[164,460,461],{"class":178},"maxProjects",[164,463,464],{"class":174},")",[164,466,417],{"class":416},[164,468,469],{"class":323},"toBe",[164,471,327],{"class":174},[164,473,474],{"class":202},"3",[164,476,397],{"class":174},[164,478,480],{"class":166,"line":479},11,[164,481,482],{"class":174},"  })\n",[164,484,486],{"class":166,"line":485},12,[164,487,488],{"class":174},"})\n",[133,490,491,492,495,496,499],{},"Pass a ",[136,493,494],{},"store"," to use another one, such as a fresh ",[136,497,498],{},"memoryStore"," with a fixed\nclock.",[141,501,503],{"id":502},"setfortest","setForTest",[133,505,506,507,509,510,513,514,517,518,520,521,524,525,528,529,417],{},"A test often needs to start somewhere the checks would not let a person go in\none step. ",[136,508,503],{}," stores values for a test as they are, past rules,\nguards, ",[136,511,512],{},"within"," and confirmations, and records them as a revision by ",[136,515,516],{},"test",".\nEach field's own check still applies: a key the group does not declare, or a\nvalue that fails its field's check, makes ",[136,519,503],{}," throw. It sets system\nvalues; put a workspace's or a person's own values in place with ",[136,522,523],{},"save"," and\nits ",[136,526,527],{},"scope",", or assign a plan with ",[136,530,531],{},"presets(id).assign",[155,533,535],{"className":157,"code":534,"language":159,"meta":160,"style":160},"import { setForTest, testTunables } from 'tunables\u002Ftesting'\n\nconst tunables = await testTunables(definitions)\nawait setForTest(tunables, { limits: { maxProjects: 1 } })\n\nawait expect(createProject(workspace, tunables)).rejects.toThrow('A workspace may have 1 open projects.')\n",[136,536,537,560,564,583,618,622],{"__ignoreMap":160},[164,538,539,541,543,546,548,550,552,554,556,558],{"class":166,"line":167},[164,540,171],{"class":170},[164,542,175],{"class":174},[164,544,545],{"class":178}," setForTest",[164,547,182],{"class":174},[164,549,216],{"class":178},[164,551,193],{"class":174},[164,553,196],{"class":170},[164,555,199],{"class":174},[164,557,138],{"class":202},[164,559,206],{"class":174},[164,561,562],{"class":166,"line":209},[164,563,274],{"emptyLinePlaceholder":273},[164,565,566,569,571,573,575,577,579,581],{"class":166,"line":229},[164,567,568],{"class":170},"const",[164,570,381],{"class":178},[164,572,289],{"class":174},[164,574,386],{"class":170},[164,576,389],{"class":323},[164,578,327],{"class":174},[164,580,394],{"class":178},[164,582,397],{"class":174},[164,584,585,587,589,591,594,596,598,601,603,605,608,610,613,615],{"class":166,"line":270},[164,586,409],{"class":170},[164,588,545],{"class":323},[164,590,327],{"class":174},[164,592,593],{"class":178},"tunables",[164,595,182],{"class":174},[164,597,175],{"class":174},[164,599,600],{"class":178}," limits",[164,602,438],{"class":174},[164,604,175],{"class":174},[164,606,607],{"class":178}," maxProjects",[164,609,438],{"class":174},[164,611,612],{"class":202}," 1",[164,614,455],{"class":174},[164,616,617],{"class":174}," })\n",[164,619,620],{"class":166,"line":277},[164,621,274],{"emptyLinePlaceholder":273},[164,623,624,626,628,630,633,635,638,640,642,645,648,650,653,655,657,660,662],{"class":166,"line":315},[164,625,409],{"class":170},[164,627,185],{"class":323},[164,629,327],{"class":174},[164,631,632],{"class":323},"createProject",[164,634,327],{"class":174},[164,636,637],{"class":178},"workspace",[164,639,182],{"class":174},[164,641,413],{"class":178},[164,643,644],{"class":174},")).",[164,646,647],{"class":178},"rejects",[164,649,417],{"class":416},[164,651,652],{"class":323},"toThrow",[164,654,327],{"class":174},[164,656,330],{"class":174},[164,658,659],{"class":202},"A workspace may have 1 open projects.",[164,661,330],{"class":174},[164,663,397],{"class":174},[141,665,667],{"id":666},"testing-your-checks","Testing your checks",[133,669,670,671,673],{},"Test each rule, guard and confirmation through ",[136,672,523],{},", the way a person meets\nit: refused with its problem, or asked, then saved once confirmed.",[155,675,677],{"className":157,"code":676,"language":159,"meta":160,"style":160},"import { projectsWithinLimit } from '..\u002Fserver\u002Fguards'\n\nit('refuses to lower the task limit below a project', async () => {\n  const tunables = await testTunables({ ...definitions, guards: { limits: [projectsWithinLimit] } })\n  const outcome = await tunables.save('limits', { maxTasksPerProject: 100 })\n  expect(outcome.saved).toBe(false)\n  expect(outcome.problems[0]?.message).toBe('Project \"Launch\" has 140 open tasks, above the 100 allowed.')\n})\n\nit('asks before turning on Guests', async () => {\n  const tunables = await testTunables(definitions)\n  const acme = { workspace: 'acme' }\n  const asked = await tunables.save('workspace', { guestAccess: true }, { scope: acme })\n  expect(asked.confirm).toMatch(\u002FGuests will see every task\u002F)\n  const saved = await tunables.save('workspace', { guestAccess: true }, { scope: acme, confirmed: true })\n  expect(saved.saved).toBe(true)\n})\n\nit('keeps a workspace within its plan', async () => {\n  const tunables = await testTunables(definitions)\n  await tunables.presets('plans').assign('acme', 'plan', 'pro')\n  const outcome = await tunables.save('limits', { maxProjects: 60 }, { scope: { workspace: 'acme' } })\n  expect(outcome.problems[0]?.message).toBe('Projects may be at most 50 for Workspace acme, the value it inherits.')\n})\n",[136,678,679,699,703,727,774,813,841,883,887,891,914,932,957,1008,1042,1100,1126,1131,1136,1160,1179,1236,1296,1334],{"__ignoreMap":160},[164,680,681,683,685,688,690,692,694,697],{"class":166,"line":167},[164,682,171],{"class":170},[164,684,175],{"class":174},[164,686,687],{"class":178}," projectsWithinLimit ",[164,689,193],{"class":174},[164,691,196],{"class":170},[164,693,199],{"class":174},[164,695,696],{"class":202},"..\u002Fserver\u002Fguards",[164,698,206],{"class":174},[164,700,701],{"class":166,"line":209},[164,702,274],{"emptyLinePlaceholder":273},[164,704,705,708,710,712,715,717,719,721,723,725],{"class":166,"line":229},[164,706,707],{"class":323},"it",[164,709,327],{"class":174},[164,711,330],{"class":174},[164,713,714],{"class":202},"refuses to lower the task limit below a project",[164,716,330],{"class":174},[164,718,182],{"class":174},[164,720,366],{"class":170},[164,722,340],{"class":174},[164,724,343],{"class":170},[164,726,346],{"class":174},[164,728,729,732,734,736,738,740,743,746,748,750,753,755,757,759,761,764,767,770,772],{"class":166,"line":270},[164,730,731],{"class":170},"  const",[164,733,381],{"class":178},[164,735,289],{"class":174},[164,737,386],{"class":170},[164,739,389],{"class":323},[164,741,742],{"class":174},"({",[164,744,745],{"class":174}," ...",[164,747,394],{"class":178},[164,749,182],{"class":174},[164,751,752],{"class":178}," guards",[164,754,438],{"class":174},[164,756,175],{"class":174},[164,758,600],{"class":178},[164,760,438],{"class":174},[164,762,763],{"class":174}," [",[164,765,766],{"class":178},"projectsWithinLimit",[164,768,769],{"class":174},"]",[164,771,455],{"class":174},[164,773,617],{"class":174},[164,775,776,778,781,783,785,787,789,791,793,795,797,799,801,803,806,808,811],{"class":166,"line":277},[164,777,731],{"class":170},[164,779,780],{"class":178}," outcome ",[164,782,289],{"class":174},[164,784,386],{"class":170},[164,786,413],{"class":412},[164,788,417],{"class":416},[164,790,523],{"class":323},[164,792,327],{"class":174},[164,794,330],{"class":174},[164,796,333],{"class":202},[164,798,330],{"class":174},[164,800,182],{"class":174},[164,802,175],{"class":174},[164,804,805],{"class":178}," maxTasksPerProject",[164,807,438],{"class":174},[164,809,810],{"class":202}," 100",[164,812,617],{"class":174},[164,814,815,818,820,823,825,828,830,832,834,836,839],{"class":166,"line":315},[164,816,817],{"class":323},"  expect",[164,819,327],{"class":174},[164,821,822],{"class":178},"outcome",[164,824,417],{"class":174},[164,826,827],{"class":178},"saved",[164,829,464],{"class":174},[164,831,417],{"class":416},[164,833,469],{"class":323},[164,835,327],{"class":174},[164,837,838],{"class":170},"false",[164,840,397],{"class":174},[164,842,843,845,847,849,851,854,857,860,863,866,868,870,872,874,876,879,881],{"class":166,"line":320},[164,844,817],{"class":323},[164,846,327],{"class":174},[164,848,822],{"class":178},[164,850,417],{"class":174},[164,852,853],{"class":178},"problems",[164,855,856],{"class":174},"[",[164,858,859],{"class":202},"0",[164,861,862],{"class":174},"]?.",[164,864,865],{"class":178},"message",[164,867,464],{"class":174},[164,869,417],{"class":416},[164,871,469],{"class":323},[164,873,327],{"class":174},[164,875,330],{"class":174},[164,877,878],{"class":202},"Project \"Launch\" has 140 open tasks, above the 100 allowed.",[164,880,330],{"class":174},[164,882,397],{"class":174},[164,884,885],{"class":166,"line":349},[164,886,488],{"class":174},[164,888,889],{"class":166,"line":375},[164,890,274],{"emptyLinePlaceholder":273},[164,892,893,895,897,899,902,904,906,908,910,912],{"class":166,"line":400},[164,894,707],{"class":323},[164,896,327],{"class":174},[164,898,330],{"class":174},[164,900,901],{"class":202},"asks before turning on Guests",[164,903,330],{"class":174},[164,905,182],{"class":174},[164,907,366],{"class":170},[164,909,340],{"class":174},[164,911,343],{"class":170},[164,913,346],{"class":174},[164,915,916,918,920,922,924,926,928,930],{"class":166,"line":479},[164,917,731],{"class":170},[164,919,381],{"class":178},[164,921,289],{"class":174},[164,923,386],{"class":170},[164,925,389],{"class":323},[164,927,327],{"class":174},[164,929,394],{"class":178},[164,931,397],{"class":174},[164,933,934,936,939,941,943,945,947,949,952,954],{"class":166,"line":485},[164,935,731],{"class":170},[164,937,938],{"class":178}," acme ",[164,940,289],{"class":174},[164,942,175],{"class":174},[164,944,443],{"class":178},[164,946,438],{"class":174},[164,948,199],{"class":174},[164,950,951],{"class":202},"acme",[164,953,330],{"class":174},[164,955,956],{"class":174}," }\n",[164,958,960,962,965,967,969,971,973,975,977,979,981,983,985,987,990,992,995,998,1000,1002,1004,1006],{"class":166,"line":959},13,[164,961,731],{"class":170},[164,963,964],{"class":178}," asked ",[164,966,289],{"class":174},[164,968,386],{"class":170},[164,970,413],{"class":412},[164,972,417],{"class":416},[164,974,523],{"class":323},[164,976,327],{"class":174},[164,978,330],{"class":174},[164,980,637],{"class":202},[164,982,330],{"class":174},[164,984,182],{"class":174},[164,986,175],{"class":174},[164,988,989],{"class":178}," guestAccess",[164,991,438],{"class":174},[164,993,994],{"class":170}," true",[164,996,997],{"class":174}," },",[164,999,175],{"class":174},[164,1001,435],{"class":178},[164,1003,438],{"class":174},[164,1005,938],{"class":178},[164,1007,488],{"class":174},[164,1009,1011,1013,1015,1018,1020,1023,1025,1027,1030,1032,1035,1038,1040],{"class":166,"line":1010},14,[164,1012,817],{"class":323},[164,1014,327],{"class":174},[164,1016,1017],{"class":178},"asked",[164,1019,417],{"class":174},[164,1021,1022],{"class":178},"confirm",[164,1024,464],{"class":174},[164,1026,417],{"class":416},[164,1028,1029],{"class":323},"toMatch",[164,1031,327],{"class":174},[164,1033,1034],{"class":174},"\u002F",[164,1036,1037],{"class":202},"Guests will see every task",[164,1039,1034],{"class":174},[164,1041,397],{"class":174},[164,1043,1045,1047,1050,1052,1054,1056,1058,1060,1062,1064,1066,1068,1070,1072,1074,1076,1078,1080,1082,1084,1086,1089,1091,1094,1096,1098],{"class":166,"line":1044},15,[164,1046,731],{"class":170},[164,1048,1049],{"class":178}," saved ",[164,1051,289],{"class":174},[164,1053,386],{"class":170},[164,1055,413],{"class":412},[164,1057,417],{"class":416},[164,1059,523],{"class":323},[164,1061,327],{"class":174},[164,1063,330],{"class":174},[164,1065,637],{"class":202},[164,1067,330],{"class":174},[164,1069,182],{"class":174},[164,1071,175],{"class":174},[164,1073,989],{"class":178},[164,1075,438],{"class":174},[164,1077,994],{"class":170},[164,1079,997],{"class":174},[164,1081,175],{"class":174},[164,1083,435],{"class":178},[164,1085,438],{"class":174},[164,1087,1088],{"class":178}," acme",[164,1090,182],{"class":174},[164,1092,1093],{"class":178}," confirmed",[164,1095,438],{"class":174},[164,1097,994],{"class":170},[164,1099,617],{"class":174},[164,1101,1103,1105,1107,1109,1111,1113,1115,1117,1119,1121,1124],{"class":166,"line":1102},16,[164,1104,817],{"class":323},[164,1106,327],{"class":174},[164,1108,827],{"class":178},[164,1110,417],{"class":174},[164,1112,827],{"class":178},[164,1114,464],{"class":174},[164,1116,417],{"class":416},[164,1118,469],{"class":323},[164,1120,327],{"class":174},[164,1122,1123],{"class":170},"true",[164,1125,397],{"class":174},[164,1127,1129],{"class":166,"line":1128},17,[164,1130,488],{"class":174},[164,1132,1134],{"class":166,"line":1133},18,[164,1135,274],{"emptyLinePlaceholder":273},[164,1137,1139,1141,1143,1145,1148,1150,1152,1154,1156,1158],{"class":166,"line":1138},19,[164,1140,707],{"class":323},[164,1142,327],{"class":174},[164,1144,330],{"class":174},[164,1146,1147],{"class":202},"keeps a workspace within its plan",[164,1149,330],{"class":174},[164,1151,182],{"class":174},[164,1153,366],{"class":170},[164,1155,340],{"class":174},[164,1157,343],{"class":170},[164,1159,346],{"class":174},[164,1161,1163,1165,1167,1169,1171,1173,1175,1177],{"class":166,"line":1162},20,[164,1164,731],{"class":170},[164,1166,381],{"class":178},[164,1168,289],{"class":174},[164,1170,386],{"class":170},[164,1172,389],{"class":323},[164,1174,327],{"class":174},[164,1176,394],{"class":178},[164,1178,397],{"class":174},[164,1180,1182,1185,1187,1189,1192,1194,1196,1199,1201,1203,1205,1208,1210,1212,1214,1216,1218,1220,1223,1225,1227,1229,1232,1234],{"class":166,"line":1181},21,[164,1183,1184],{"class":170},"  await",[164,1186,413],{"class":412},[164,1188,417],{"class":416},[164,1190,1191],{"class":323},"presets",[164,1193,327],{"class":174},[164,1195,330],{"class":174},[164,1197,1198],{"class":202},"plans",[164,1200,330],{"class":174},[164,1202,464],{"class":174},[164,1204,417],{"class":416},[164,1206,1207],{"class":323},"assign",[164,1209,327],{"class":174},[164,1211,330],{"class":174},[164,1213,951],{"class":202},[164,1215,330],{"class":174},[164,1217,182],{"class":174},[164,1219,199],{"class":174},[164,1221,1222],{"class":202},"plan",[164,1224,330],{"class":174},[164,1226,182],{"class":174},[164,1228,199],{"class":174},[164,1230,1231],{"class":202},"pro",[164,1233,330],{"class":174},[164,1235,397],{"class":174},[164,1237,1239,1241,1243,1245,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1269,1272,1274,1276,1278,1280,1282,1284,1286,1288,1290,1292,1294],{"class":166,"line":1238},22,[164,1240,731],{"class":170},[164,1242,780],{"class":178},[164,1244,289],{"class":174},[164,1246,386],{"class":170},[164,1248,413],{"class":412},[164,1250,417],{"class":416},[164,1252,523],{"class":323},[164,1254,327],{"class":174},[164,1256,330],{"class":174},[164,1258,333],{"class":202},[164,1260,330],{"class":174},[164,1262,182],{"class":174},[164,1264,175],{"class":174},[164,1266,607],{"class":178},[164,1268,438],{"class":174},[164,1270,1271],{"class":202}," 60",[164,1273,997],{"class":174},[164,1275,175],{"class":174},[164,1277,435],{"class":178},[164,1279,438],{"class":174},[164,1281,175],{"class":174},[164,1283,443],{"class":178},[164,1285,438],{"class":174},[164,1287,199],{"class":174},[164,1289,951],{"class":202},[164,1291,330],{"class":174},[164,1293,455],{"class":174},[164,1295,617],{"class":174},[164,1297,1299,1301,1303,1305,1307,1309,1311,1313,1315,1317,1319,1321,1323,1325,1327,1330,1332],{"class":166,"line":1298},23,[164,1300,817],{"class":323},[164,1302,327],{"class":174},[164,1304,822],{"class":178},[164,1306,417],{"class":174},[164,1308,853],{"class":178},[164,1310,856],{"class":174},[164,1312,859],{"class":202},[164,1314,862],{"class":174},[164,1316,865],{"class":178},[164,1318,464],{"class":174},[164,1320,417],{"class":416},[164,1322,469],{"class":323},[164,1324,327],{"class":174},[164,1326,330],{"class":174},[164,1328,1329],{"class":202},"Projects may be at most 50 for Workspace acme, the value it inherits.",[164,1331,330],{"class":174},[164,1333,397],{"class":174},[164,1335,1337],{"class":166,"line":1336},24,[164,1338,488],{"class":174},[141,1340,1342],{"id":1341},"testing-your-server","Testing your server",[133,1344,1345,1348,1349,1352,1353,1356,1357,1360],{},[136,1346,1347],{},"createHandler"," is a plain function from a ",[136,1350,1351],{},"Request"," to a ",[136,1354,1355],{},"Response",", so a test\ncan call it without a server, and ",[136,1358,1359],{},"fetchClient"," can call it as the screens do:",[155,1362,1364],{"className":157,"code":1363,"language":159,"meta":160,"style":160},"import { createHandler } from 'tunables\u002Fserver'\nimport { fetchClient } from 'tunables\u002Fclient'\n\nconst handle = createHandler({ tunables, actor: () => 'Ada', authorize: () => true })\nconst client = fetchClient({\n  fetch: async (input, init) => (await handle(new Request(new URL(String(input), 'http:\u002F\u002Fapp.test'), init)))\n    ?? new Response('Not found', { status: 404 })\n})\n\nconst saved = await client.save('limits', { values: { maxProjects: 6 }, reason: 'Test' })\nexpect(saved.revision?.actor).toBe('Ada')\n",[136,1365,1366,1386,1406,1410,1459,1474,1545,1579,1583,1587,1646],{"__ignoreMap":160},[164,1367,1368,1370,1372,1375,1377,1379,1381,1384],{"class":166,"line":167},[164,1369,171],{"class":170},[164,1371,175],{"class":174},[164,1373,1374],{"class":178}," createHandler ",[164,1376,193],{"class":174},[164,1378,196],{"class":170},[164,1380,199],{"class":174},[164,1382,1383],{"class":202},"tunables\u002Fserver",[164,1385,206],{"class":174},[164,1387,1388,1390,1392,1395,1397,1399,1401,1404],{"class":166,"line":209},[164,1389,171],{"class":170},[164,1391,175],{"class":174},[164,1393,1394],{"class":178}," fetchClient ",[164,1396,193],{"class":174},[164,1398,196],{"class":170},[164,1400,199],{"class":174},[164,1402,1403],{"class":202},"tunables\u002Fclient",[164,1405,206],{"class":174},[164,1407,1408],{"class":166,"line":229},[164,1409,274],{"emptyLinePlaceholder":273},[164,1411,1412,1414,1417,1419,1422,1424,1426,1428,1431,1433,1435,1437,1439,1442,1444,1446,1449,1451,1453,1455,1457],{"class":166,"line":270},[164,1413,568],{"class":170},[164,1415,1416],{"class":178}," handle ",[164,1418,289],{"class":174},[164,1420,1421],{"class":323}," createHandler",[164,1423,742],{"class":174},[164,1425,413],{"class":178},[164,1427,182],{"class":174},[164,1429,1430],{"class":323}," actor",[164,1432,438],{"class":174},[164,1434,340],{"class":174},[164,1436,343],{"class":170},[164,1438,199],{"class":174},[164,1440,1441],{"class":202},"Ada",[164,1443,330],{"class":174},[164,1445,182],{"class":174},[164,1447,1448],{"class":323}," authorize",[164,1450,438],{"class":174},[164,1452,340],{"class":174},[164,1454,343],{"class":170},[164,1456,994],{"class":170},[164,1458,617],{"class":174},[164,1460,1461,1463,1466,1468,1471],{"class":166,"line":277},[164,1462,568],{"class":170},[164,1464,1465],{"class":178}," client ",[164,1467,289],{"class":174},[164,1469,1470],{"class":323}," fetchClient",[164,1472,1473],{"class":174},"({\n",[164,1475,1476,1479,1481,1483,1486,1489,1491,1494,1496,1498,1500,1502,1505,1507,1509,1512,1514,1516,1519,1521,1524,1526,1528,1531,1533,1536,1538,1540,1542],{"class":166,"line":315},[164,1477,1478],{"class":323},"  fetch",[164,1480,438],{"class":174},[164,1482,366],{"class":170},[164,1484,1485],{"class":174}," (",[164,1487,1488],{"class":178},"input",[164,1490,182],{"class":174},[164,1492,1493],{"class":178}," init",[164,1495,464],{"class":174},[164,1497,343],{"class":170},[164,1499,1485],{"class":174},[164,1501,409],{"class":170},[164,1503,1504],{"class":323}," handle",[164,1506,327],{"class":174},[164,1508,450],{"class":170},[164,1510,1511],{"class":323}," Request",[164,1513,327],{"class":174},[164,1515,450],{"class":170},[164,1517,1518],{"class":323}," URL",[164,1520,327],{"class":174},[164,1522,1523],{"class":323},"String",[164,1525,327],{"class":174},[164,1527,1488],{"class":178},[164,1529,1530],{"class":174},"),",[164,1532,199],{"class":174},[164,1534,1535],{"class":202},"http:\u002F\u002Fapp.test",[164,1537,330],{"class":174},[164,1539,1530],{"class":174},[164,1541,1493],{"class":178},[164,1543,1544],{"class":174},")))\n",[164,1546,1547,1550,1553,1556,1558,1560,1563,1565,1567,1569,1572,1574,1577],{"class":166,"line":320},[164,1548,1549],{"class":174},"    ??",[164,1551,1552],{"class":170}," new",[164,1554,1555],{"class":323}," Response",[164,1557,327],{"class":174},[164,1559,330],{"class":174},[164,1561,1562],{"class":202},"Not found",[164,1564,330],{"class":174},[164,1566,182],{"class":174},[164,1568,175],{"class":174},[164,1570,1571],{"class":178}," status",[164,1573,438],{"class":174},[164,1575,1576],{"class":202}," 404",[164,1578,617],{"class":174},[164,1580,1581],{"class":166,"line":349},[164,1582,488],{"class":174},[164,1584,1585],{"class":166,"line":375},[164,1586,274],{"emptyLinePlaceholder":273},[164,1588,1589,1591,1593,1595,1597,1600,1602,1604,1606,1608,1610,1612,1614,1616,1619,1621,1623,1625,1627,1630,1632,1635,1637,1639,1642,1644],{"class":166,"line":400},[164,1590,568],{"class":170},[164,1592,1049],{"class":178},[164,1594,289],{"class":174},[164,1596,386],{"class":170},[164,1598,1599],{"class":412}," client",[164,1601,417],{"class":416},[164,1603,523],{"class":323},[164,1605,327],{"class":174},[164,1607,330],{"class":174},[164,1609,333],{"class":202},[164,1611,330],{"class":174},[164,1613,182],{"class":174},[164,1615,175],{"class":174},[164,1617,1618],{"class":178}," values",[164,1620,438],{"class":174},[164,1622,175],{"class":174},[164,1624,607],{"class":178},[164,1626,438],{"class":174},[164,1628,1629],{"class":202}," 6",[164,1631,997],{"class":174},[164,1633,1634],{"class":178}," reason",[164,1636,438],{"class":174},[164,1638,199],{"class":174},[164,1640,1641],{"class":202},"Test",[164,1643,330],{"class":174},[164,1645,617],{"class":174},[164,1647,1648,1651,1653,1655,1657,1660,1663,1666,1668,1670,1672,1674,1676,1678,1680],{"class":166,"line":479},[164,1649,1650],{"class":323},"expect",[164,1652,327],{"class":174},[164,1654,827],{"class":178},[164,1656,417],{"class":174},[164,1658,1659],{"class":178},"revision",[164,1661,1662],{"class":174},"?.",[164,1664,1665],{"class":178},"actor",[164,1667,464],{"class":174},[164,1669,417],{"class":416},[164,1671,469],{"class":323},[164,1673,327],{"class":174},[164,1675,330],{"class":174},[164,1677,1441],{"class":202},[164,1679,330],{"class":174},[164,1681,397],{"class":174},[141,1683,1685],{"id":1684},"postgres-in-tests","Postgres in tests",[133,1687,1688,1689,1692,1693,1696,1697,1700],{},"The memory store behaves like Postgres for one process: transactions are\nserialized and roll back on error. Test ",[136,1690,1691],{},"hold"," and concurrent changes against\na real database through ",[136,1694,1695],{},"postgresStore",", in a schema of their own that each\ntest creates with ",[136,1698,1699],{},"migrate"," and drops after.",[1702,1703,1704],"style",{},"html pre.shiki code .sZ4mS, html code.shiki .sZ4mS{--shiki-light:#115E59;--shiki-default:#115E59;--shiki-dark:#5EEAD4}html pre.shiki code .sp9ci, html code.shiki .sp9ci{--shiki-light:#4F5D63;--shiki-default:#4F5D63;--shiki-dark:#A3AFB4}html pre.shiki code .sVz71, html code.shiki .sVz71{--shiki-light:#182025;--shiki-default:#182025;--shiki-dark:#E6EBEC}html pre.shiki code .sjCzm, html code.shiki .sjCzm{--shiki-light:#92400E;--shiki-default:#92400E;--shiki-dark:#FCD34D}html pre.shiki code .sOThN, html code.shiki .sOThN{--shiki-light:#2A3439;--shiki-light-font-weight:bold;--shiki-default:#2A3439;--shiki-default-font-weight:bold;--shiki-dark:#F6F8F8;--shiki-dark-font-weight:bold}html pre.shiki code .sq0K3, html code.shiki .sq0K3{--shiki-light:#182025;--shiki-light-font-weight:bold;--shiki-default:#182025;--shiki-default-font-weight:bold;--shiki-dark:#E6EBEC;--shiki-dark-font-weight:bold}html pre.shiki code .sBd0f, html code.shiki .sBd0f{--shiki-light:#4F5D63;--shiki-light-font-weight:bold;--shiki-default:#4F5D63;--shiki-default-font-weight:bold;--shiki-dark:#A3AFB4;--shiki-dark-font-weight:bold}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":160,"searchDepth":209,"depth":209,"links":1706},[1707,1708,1709,1710,1711],{"id":143,"depth":209,"text":144},{"id":502,"depth":209,"text":503},{"id":666,"depth":209,"text":667},{"id":1341,"depth":209,"text":1342},{"id":1684,"depth":209,"text":1685},"md",{},{"title":65,"description":66},"docs\u002Ftesting","RfJbHAWtsanKXBsiJCgzTPia4l4ydc900ZcCEdw07zk",1792845119850]