Solutions Reference
How to Use This Page
This page contains ideal solutions for all tasks in the workshop. Use this reference if:
- Your code isn't producing the expected output
- You want to compare your approach with the ideal solution
- You're stuck and need to see what the final result should look like
Important: Try to complete each task on your own first using Copilot. This reference is for troubleshooting and learning, not for copying solutions.
Part 1: Getting Started with Copilot
Part 1, Task 1: Build the Blueprint Decoder
Create a decoder to extract secrets marked between {* and *} from a blueprint file. Here's what the ideal result looks like for each language:
File: blueprint_decoder.py
import re
# Function to read a blueprint file and extract all secrets marked between {* and *}
# Example: League Blueprint contains {* AGENT_CODENAME: SHADOWMIND *}
# Should extract: "AGENT_CODENAME: SHADOWMIND" (without the markers)
# Uses regex pattern to find all occurrences
# Returns a list of extracted secrets
def decode_blueprint(filename):
with open(filename, 'r') as file:
content = file.read()
# Use regex to find all secrets between {* and *}
pattern = r'\{\* (.*?) \*\}'
secrets = re.findall(pattern, content)
return secrets
# Test the decoder
if __name__ == "__main__":
secrets = decode_blueprint("blueprint-data.txt")
print(f"Found {len(secrets)} secret(s):")
for i, secret in enumerate(secrets, 1):
print(f"{i}. {secret}")
Expected Output:
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
File: BlueprintDecoder.cs
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Generic;
public class BlueprintDecoder
{
/// <summary>
/// Reads a blueprint file and extracts all secrets marked between {* and *}
/// Example: {* VAULT_ACCESS_CODE: DELTA-7-7-ECHO *} extracts the code inside
/// Uses regex pattern to find all occurrences
/// </summary>
/// <param name="filename">Path to the blueprint file</param>
/// <returns>List of extracted secret strings</returns>
public static List<string> DecodeBlueprint(string filename)
{
string content = File.ReadAllText(filename);
// Use regex to find all secrets between {* and *}
string pattern = @"\{\* (.*?) \*\}";
MatchCollection matches = Regex.Matches(content, pattern);
List<string> secrets = new List<string>();
foreach (Match match in matches)
{
secrets.Add(match.Groups[1].Value);
}
return secrets;
}
static void Main()
{
var secrets = DecodeBlueprint("blueprint-data.txt");
Console.WriteLine($"Found {secrets.Count} secret(s):");
for (int i = 0; i < secrets.Count; i++)
{
Console.WriteLine($"{i + 1}. {secrets[i]}");
}
}
}
Expected Output:
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
File: blueprint_decoder.js
const fs = require('fs');
/**
* Reads a blueprint file and extracts all secrets marked between {* and *}
* Example: League transmission contains {* EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE *}
* Extracts: "EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE" (without markers)
* Uses regex pattern with match() or matchAll()
* @param {string} filename - Path to the blueprint file
* @returns {Array<string>} Array of extracted secret strings
*/
function decodeBlueprint(filename) {
const content = fs.readFileSync(filename, 'utf8');
// Use regex to find all secrets between {* and *}
const pattern = /\{\* (.*?) \*\}/g;
const matches = [...content.matchAll(pattern)];
const secrets = matches.map(match => match[1]);
return secrets;
}
// Test the decoder
const secrets = decodeBlueprint('blueprint-data.txt');
console.log(`Found ${secrets.length} secret(s):`);
secrets.forEach((secret, index) => {
console.log(`${index + 1}. ${secret}`);
});
Expected Output:
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
Part 1, Task 2: Add Error Handling
Gracefully handle missing files:
File: blueprint_decoder.py (with error handling)
import re
def decode_blueprint(filename):
with open(filename, 'r') as file:
content = file.read()
pattern = r'\{\* (.*?) \*\}'
secrets = re.findall(pattern, content)
return secrets
# Enhanced version with error handling
# If file doesn't exist, return an empty list and print an error message
def decode_blueprint_safe(filename):
try:
return decode_blueprint(filename)
except FileNotFoundError:
print(f"Error: File '{filename}' not found.")
return []
if __name__ == "__main__":
# Test error handling
secrets = decode_blueprint_safe("nonexistent.txt")
print(f"Found {len(secrets)} secrets") # Should print 0
# Test normal operation
secrets = decode_blueprint_safe("blueprint-data.txt")
print(f"Found {len(secrets)} secrets") # Should print 5
Expected Output:
Error: File 'nonexistent.txt' not found.
Found 0 secrets
Found 5 secrets
File: BlueprintDecoder.cs (with error handling)
using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Generic;
public class BlueprintDecoder
{
public static List<string> DecodeBlueprint(string filename)
{
string content = File.ReadAllText(filename);
string pattern = @"\{\* (.*?) \*\}";
MatchCollection matches = Regex.Matches(content, pattern);
List<string> secrets = new List<string>();
foreach (Match match in matches)
{
secrets.Add(match.Groups[1].Value);
}
return secrets;
}
// Enhanced version with error handling
// If file doesn't exist, return empty list and print error message
public static List<string> DecodeBlueprintSafe(string filename)
{
try
{
return DecodeBlueprint(filename);
}
catch (FileNotFoundException)
{
Console.WriteLine($"Error: File '{filename}' not found.");
return new List<string>();
}
}
static void Main()
{
// Test error handling
var secrets1 = DecodeBlueprintSafe("nonexistent.txt");
Console.WriteLine($"Found {secrets1.Count} secrets");
// Test normal operation
var secrets2 = DecodeBlueprintSafe("blueprint-data.txt");
Console.WriteLine($"Found {secrets2.Count} secrets");
}
}
Expected Output:
Error: File 'nonexistent.txt' not found.
Found 0 secrets
Found 5 secrets
File: blueprint_decoder.js (with error handling)
const fs = require('fs');
function decodeBlueprint(filename) {
const content = fs.readFileSync(filename, 'utf8');
const pattern = /\{\* (.*?) \*\}/g;
const matches = [...content.matchAll(pattern)];
const secrets = matches.map(match => match[1]);
return secrets;
}
// Enhanced version with error handling
// If file doesn't exist, return empty array and print error message
function decodeBlueprintSafe(filename) {
try {
return decodeBlueprint(filename);
} catch (err) {
console.log(`Error: File '${filename}' not found.`);
return [];
}
}
// Test error handling
const secrets1 = decodeBlueprintSafe('nonexistent.txt');
console.log(`Found ${secrets1.length} secrets`);
// Test normal operation
const secrets2 = decodeBlueprintSafe('blueprint-data.txt');
console.log(`Found ${secrets2.length} secrets`);
Expected Output:
Error: File 'nonexistent.txt' not found.
Found 0 secrets
Found 5 secrets
Part 1, Task 3: Format the Output
Display secrets in a nicely formatted report:
Continuation of blueprint_decoder.py:
# Function to format and display secrets in a nice report format
# Shows total count, numbered list, and a separator line
def display_secrets_report(secrets):
separator = "=" * 40
print(separator)
print("DECODED SECRETS REPORT")
print(separator)
print(f"Found {len(secrets)} secret(s):\n")
for i, secret in enumerate(secrets, 1):
print(f"{i}. {secret}")
print(separator)
# Use it
secrets = decode_blueprint_safe("blueprint-data.txt")
display_secrets_report(secrets)
Expected Output:
========================================
DECODED SECRETS REPORT
========================================
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
========================================
Continuation of BlueprintDecoder.cs:
// Function to format and display secrets in a nice report format
// Shows total count, numbered list, and separator line
public static void DisplaySecretsReport(List<string> secrets)
{
string separator = new string('=', 40);
Console.WriteLine(separator);
Console.WriteLine("DECODED SECRETS REPORT");
Console.WriteLine(separator);
Console.WriteLine($"Found {secrets.Count} secret(s):\n");
for (int i = 0; i < secrets.Count; i++)
{
Console.WriteLine($"{i + 1}. {secrets[i]}");
}
Console.WriteLine(separator);
}
// Modified Main to use the report
static void Main()
{
var secrets = DecodeBlueprintSafe("blueprint-data.txt");
DisplaySecretsReport(secrets);
}
Expected Output:
========================================
DECODED SECRETS REPORT
========================================
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
========================================
Continuation of blueprint_decoder.js:
// Function to format and display secrets in a nice report format
// Shows total count, numbered list, and separator line
function displaySecretsReport(secrets) {
const separator = '='.repeat(40);
console.log(separator);
console.log('DECODED SECRETS REPORT');
console.log(separator);
console.log(`Found ${secrets.length} secret(s):\n`);
secrets.forEach((secret, index) => {
console.log(`${index + 1}. ${secret}`);
});
console.log(separator);
}
// Use it
const secrets = decodeBlueprintSafe('blueprint-data.txt');
displaySecretsReport(secrets);
Expected Output:
========================================
DECODED SECRETS REPORT
========================================
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
========================================
Part 1, Task 4: Categorize Secrets by Type
Extract and count secret types (the word before the colon):
Continuation of blueprint_decoder.py:
# Function to categorize secrets by their type (word before the colon)
def categorize_secrets(secrets):
categories = {}
for secret in secrets:
if ':' in secret:
category = secret.split(':')[0].strip()
else:
category = "UNCLASSIFIED"
if category not in categories:
categories[category] = 0
categories[category] += 1
return categories
# Show categorization
secrets = decode_blueprint_safe("blueprint-data.txt")
categories = categorize_secrets(secrets)
print("\nSecret Categories:")
for category, count in sorted(categories.items()):
print(f" {category}: {count}")
Expected Output:
Secret Categories:
AGENT_CODENAME: 1
EMERGENCY_PROTOCOL: 1
MEETING_LOCATION: 1
SECURE_COMMS_PROTOCOL: 1
VAULT_ACCESS_CODE: 1
Continuation of BlueprintDecoder.cs:
// Function to categorize secrets by their type (word before the colon)
public static Dictionary<string, int> CategorizeSecrets(List<string> secrets)
{
var categories = new Dictionary<string, int>();
foreach (var secret in secrets)
{
string category;
if (secret.Contains(':'))
{
category = secret.Split(':')[0].Trim();
}
else
{
category = "UNCLASSIFIED";
}
if (!categories.ContainsKey(category))
{
categories[category] = 0;
}
categories[category]++;
}
return categories;
}
// Modified Main to show categorization
static void Main()
{
var secrets = DecodeBlueprintSafe("blueprint-data.txt");
DisplaySecretsReport(secrets);
var categories = CategorizeSecrets(secrets);
Console.WriteLine("\nSecret Categories:");
foreach (var category in categories)
{
Console.WriteLine($" {category.Key}: {category.Value}");
}
}
Expected Output:
========================================
DECODED SECRETS REPORT
========================================
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
========================================
Secret Categories:
AGENT_CODENAME: 1
EMERGENCY_PROTOCOL: 1
MEETING_LOCATION: 1
SECURE_COMMS_PROTOCOL: 1
VAULT_ACCESS_CODE: 1
Continuation of blueprint_decoder.js:
// Function to categorize secrets by their type (word before the colon)
function categorizeSecrets(secrets) {
const categories = {};
secrets.forEach(secret => {
let category;
if (secret.includes(':')) {
category = secret.split(':')[0].trim();
} else {
category = 'UNCLASSIFIED';
}
if (!categories[category]) {
categories[category] = 0;
}
categories[category]++;
});
return categories;
}
// Show categorization
const secrets = decodeBlueprintSafe('blueprint-data.txt');
displaySecretsReport(secrets);
const categories = categorizeSecrets(secrets);
console.log('\nSecret Categories:');
Object.keys(categories).sort().forEach(category => {
console.log(` ${category}: ${categories[category]}`);
});
Expected Output:
========================================
DECODED SECRETS REPORT
========================================
Found 5 secret(s):
1. SECURE_COMMS_PROTOCOL
2. AGENT_CODENAME: SHADOWMIND
3. VAULT_ACCESS_CODE: DELTA-7-7-ECHO
4. MEETING_LOCATION: SAFEHOUSE_BERLIN_CHECKPOINT_C
5. EMERGENCY_PROTOCOL: NIGHTFALL_SEQUENCE_ACTIVE
========================================
Secret Categories:
AGENT_CODENAME: 1
EMERGENCY_PROTOCOL: 1
MEETING_LOCATION: 1
SECURE_COMMS_PROTOCOL: 1
VAULT_ACCESS_CODE: 1
Part 2: Mental Models & Context
These tasks are designed to show a visible, measurable difference in Copilot's suggestions based on the context you provide. The code below shows both what you type and what Copilot is expected to generate in response.
Part 2, Task 1: Context Sensitivity
The goal is to see Copilot give a meaningless suggestion with minimal context, then a precise one after you add types and a clear comment.
Step 1 - What you type (vague):
def transform(data):
# TODO: implement
What Copilot typically suggests: A generic stub like return data or a bare pass - it has nothing to go on.
Step 2 - What you type (with types + clear comment):
def transform(data: List[Dict[str, Any]]) -> List[str]:
# Extract the 'name' field from each dictionary and return as a list of strings
What Copilot should suggest:
return [item['name'] for item in data]
Why: The return type List[str], the parameter type List[Dict[str, Any]], and the comment stating exactly what to extract all combine to give Copilot an unambiguous target. The list comprehension is the natural, idiomatic result.
Step 1 - What you type (vague):
public List<string> Process(List<object> data)
{
// TODO: implement
}
What Copilot typically suggests: A stub returning new List<string>() or throw new NotImplementedException() - no useful logic.
Step 2 - What you type (with specific types + clear comment):
public List<string> ExtractNames(List<Dictionary<string, object>> data)
{
// Extract the 'name' field from each dictionary and return as a list of strings
}
What Copilot should suggest:
return data.Select(d => d["name"].ToString()).ToList();
Why: The concrete generic types tell Copilot the shape of the input, and the comment tells it exactly which field to extract. LINQ's Select is the idiomatic C# answer and Copilot has enough signal to reach it.
Step 1 - What you type (vague):
function transform(data) {
// TODO: implement
}
What Copilot typically suggests: return data; or nothing useful.
Step 2 - What you type (with JSDoc + clear comment):
/**
* @param {{ name: string }[]} data
* @returns {string[]}
*/
function extractNames(data) {
// Extract the 'name' field from each object and return as a list of strings
}
What Copilot should suggest:
return data.map(item => item.name);
Why: The JSDoc type {{ name: string }[]} tells Copilot the input is an array of objects with a name property. Combined with the return type string[] and the comment, it has everything it needs to generate the correct map expression.
Part 2, Task 2: Variable Name Context
The goal is to see Copilot's suggestion quality shift dramatically when you change a single thing: the names. Same intent, different signal - different output.
Step 1 - What you type (vague names):
def process(d):
# return the result
What Copilot typically suggests:
return transform(d)
A generic pass-through with an invented helper function - Copilot is guessing, because d and process carry no semantic meaning.
Step 2 - What you type (descriptive names + types):
def extract_product_names(products: List[Product]) -> List[str]:
# iterate over products and return a list of each product's name
What Copilot should suggest:
return [product.name for product in products]
Why: The function name (extract_product_names), the parameter name (products), the type (List[Product]), and the return type (List[str]) together form a complete picture. Copilot doesn't need to guess - the list comprehension accessing .name is the only logical outcome.
Step 1 - What you type (vague names):
public List<string> Process(List<object> d)
{
// return the result
}
What Copilot typically suggests:
return d.Select(x => x.ToString()).ToList();
A generic ToString() fallback - Copilot has no idea what d contains, so it takes the safest possible route.
Step 2 - What you type (descriptive names + types):
public List<string> ExtractProductNames(List<Product> products)
{
// iterate over products and return a list of each product's name
}
What Copilot should suggest:
return products.Select(p => p.Name).ToList();
Why: The method name makes the intent unambiguous. The typed parameter List<Product> tells Copilot to access a known model property, and it produces idiomatic LINQ accessing .Name instead of a generic ToString().
Step 1 - What you type (vague names):
function process(d) {
// return the result
}
What Copilot typically suggests:
return d;
A bare passthrough - d tells Copilot nothing, so it produces nothing useful.
Step 2 - What you type (descriptive names + JSDoc):
/**
* @param {{ name: string }[]} items
* @returns {string[]}
*/
function extractItemNames(items) {
// iterate over items and return a list of each item's name
}
What Copilot should suggest:
return items.map(item => item.name);
Why: The JSDoc {{ name: string }[]} tells Copilot the exact shape of the array elements. The function name, parameter name, and comment all reinforce the same intent. The result is a precise, idiomatic map accessing the correct property.
Part 2, Task 3: Example-Driven Prompting
The goal is to demonstrate that Copilot picks up on patterns you establish and carries them forward to new functions.
Step 1 - Establish the pattern:
def format_names(names: List[str]) -> List[str]:
# Example: ["alice", "bob"] -> ["ALICE", "BOB"]
return [name.upper() for name in names]
Step 2 - Hint you want the same pattern applied:
def format_dates(dates: List[str]) -> List[str]:
# Using the same pattern as format_names above:
# Example: ["2026-01-17", "2026-02-20"] -> ["Jan 17, 2026", "Feb 20, 2026"]
What Copilot should suggest:
return [datetime.strptime(d, '%Y-%m-%d').strftime('%b %d, %Y') for d in dates]
Why: Copilot sees the established list comprehension pattern in format_names and the example transformation in your comment. It keeps the same structure and substitutes the appropriate datetime formatting logic.
If Copilot suggested something different - like a try/except loop instead of a list comprehension: that's intentional behaviour, not a mistake. Copilot has built-in tendencies around safety, error handling, and robust code. Date parsing is a well-known source of runtime errors - a malformed string like "not-a-date" will crash strptime without protection. Copilot recognises this risk and applies defensive coding even when the established pattern didn't include it.
This reflects one of Copilot's core design principles: it balances following your context against its broader responsibility around security, privacy, and compliance. It won't blindly repeat an unsafe pattern just because you showed it one. Think of it as a code reviewer who spots a gap you didn't notice - not a deviation from your instructions, but a quiet improvement in your best interest.
You can still guide it back to a pure list comprehension by adding a comment like # assume all dates are valid ISO format strings, which signals that safe input is guaranteed and the defensive branch is unnecessary.
Step 1 - Establish the pattern:
public List<string> FormatNames(List<string> names)
{
// Example: ["alice", "bob"] -> ["ALICE", "BOB"]
return names.Select(n => n.ToUpper()).ToList();
}
Step 2 - Hint you want the same pattern applied:
public List<string> FormatDates(List<string> dates)
{
// Using the same pattern as FormatNames above:
// Example: ["2026-01-17", "2026-02-20"] -> ["Jan 17, 2026", "Feb 20, 2026"]
}
What Copilot should suggest:
return dates.Select(d => DateTime.Parse(d).ToString("MMM dd, yyyy")).ToList();
Why: Copilot mirrors the LINQ Select(...).ToList() chain it saw in FormatNames, substituting date parsing for the string transform. The explicit example in the comment anchors the exact output format.
If Copilot suggested a safer variant - like wrapping the parse in a try/catch or using DateTime.TryParse instead: that's by design, not a deviation. DateTime.Parse throws a FormatException on invalid input, and Copilot is aware of this. It may decide that the safer overload or a guard clause is more appropriate than following your LINQ pattern verbatim.
This reflects Copilot's built-in focus on security, reliability, and compliance. It tries to keep your code safe even when the context you provided didn't account for edge cases. If you want it to stay with the clean LINQ chain, add a comment like // input is pre-validated ISO 8601 strings to signal that defensive parsing is unnecessary here.
Step 1 - Establish the pattern:
// Example: ["alice", "bob"] -> ["ALICE", "BOB"]
const formatNames = (names) => names.map((name) => name.toUpperCase());
Step 2 - Hint you want the same pattern applied:
// Using the same pattern as formatNames above:
// Example: ["2026-01-17", "2026-02-20"] -> ["Jan 17, 2026", "Feb 20, 2026"]
const formatDates = (dates) => {
What Copilot should suggest:
return dates.map((d) =>
new Date(d).toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' })
);
Why: Copilot sees the arrow function / .map() style established in formatNames. The example transformation tells it the target format and it applies toLocaleDateString - the natural JS idiom for that output.
If Copilot wrapped the body in a try/catch or added input validation instead of a clean .map(): that's Copilot applying its built-in judgment about safety and robustness. new Date() silently produces an Invalid Date object for bad input, and toLocaleDateString on it returns the string "Invalid Date" - a subtle bug that could slip through unnoticed. Copilot may choose to guard against this even when your established pattern didn't.
This is part of Copilot's design: it holds a baseline responsibility for security, privacy, and compliance that can override strict pattern-following when it believes the pattern carries risk. To keep it in pure .map() style, be explicit: add a comment like // dates are guaranteed valid ISO 8601 strings, removing the ambiguity that triggered the defensive suggestion.
Part 2, Task 4: Docstring Impact
The goal is to show how a detailed docstring alone - without changing parameter names - is enough to direct Copilot to a specific, non-obvious algorithm (Heron's formula).
Step 1 - What you type (no docstring):
def calculate(a, b, c):
What Copilot typically suggests: Something generic like return a + b + c or a bare pass. Three float parameters named a, b, c give it almost nothing to go on.
Step 2 - What you type (with detailed docstring):
def calculate(a: float, b: float, c: float) -> float:
"""
Calculate the area of a triangle given three side lengths using Heron's formula.
Args:
a: Length of side A
b: Length of side B
c: Length of side C
Returns:
The area of the triangle
"""
What Copilot should suggest:
s = (a + b + c) / 2
return (s * (s - a) * (s - b) * (s - c)) ** 0.5
Why: The docstring names the exact algorithm ("Heron's formula") and describes the inputs precisely. Copilot matches that to a well-known formula and generates the correct two-line implementation - even though the parameter names a, b, c are minimal.
Step 1 - What you type (no documentation):
public double Calculate(double a, double b, double c)
What Copilot typically suggests: return a + b + c; or a throw new NotImplementedException(); stub - no algorithm-specific logic.
Step 2 - What you type (with XML documentation):
/// <summary>
/// Calculate the area of a triangle given three side lengths using Heron's formula.
/// </summary>
/// <param name="a">Length of side A</param>
/// <param name="b">Length of side B</param>
/// <param name="c">Length of side C</param>
/// <returns>The area of the triangle</returns>
public double Calculate(double a, double b, double c)
What Copilot should suggest:
{
double s = (a + b + c) / 2;
return Math.Sqrt(s * (s - a) * (s - b) * (s - c));
}
Why: The XML doc names Heron's formula explicitly. Copilot produces the correct semi-perimeter calculation and Math.Sqrt call, which it would never have inferred from the bare signature alone.
Step 1 - What you type (no documentation):
function calculate(a, b, c) {
// Copilot has little context; suggestions will be generic
}
What Copilot typically suggests: return a + b + c; or similar - no specific algorithm.
Step 2 - What you type (with JSDoc):
/**
* Calculate the area of a triangle given three side lengths using Heron's formula.
* @param {number} a Length of side A
* @param {number} b Length of side B
* @param {number} c Length of side C
* @returns {number} The area of the triangle
*/
function calculate(a, b, c) {
What Copilot should suggest:
const s = (a + b + c) / 2;
return Math.sqrt(s * (s - a) * (s - b) * (s - c));
}
Why: Naming the algorithm in the JSDoc is the entire signal. Copilot retrieves the well-known Heron's formula pattern and produces the correct Math.sqrt implementation - demonstrating that documentation isn't just for humans.
Part 3: Pair Programming
Part 3 tasks focus on using Copilot as a true pair programmer - guiding it through refactoring, test generation, and documentation. The examples below show what ideal solutions look like for each task.
Part 3, Task 1: Refactoring with Copilot
The goal is to prompt Copilot to extract helper functions and reduce duplication. Here's an example of before and after refactoring the blueprint decoder:
Before (monolithic function):
def decode_blueprint(filename):
try:
with open(filename, 'r') as f:
content = f.read()
import re
secrets = re.findall(r'\{\* (.*?) \*\}', content)
print("=" * 40)
print("DECODED SECRETS REPORT")
print("=" * 40)
print(f"Found {len(secrets)} secret(s):\n")
for i, s in enumerate(secrets, 1):
print(f"{i}. {s}")
print("=" * 40)
cats = {}
for s in secrets:
key = s.split(":")[0].strip() if ":" in s else "UNKNOWN"
cats[key] = cats.get(key, 0) + 1
return cats
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
return {}
Refactoring prompt to give Copilot:
# Refactor the function above into three separate helper functions:
# 1. read_blueprint(filename) -> str - reads and returns file content
# 2. extract_secrets(content: str) -> List[str] - uses regex to find all {* ... *}
# 3. categorize_secrets(secrets: List[str]) -> Dict[str, int] - counts by prefix before ":"
After (refactored - what Copilot should generate):
import re
from typing import List, Dict
def read_blueprint(filename: str) -> str:
"""Read and return the raw content of a blueprint file."""
with open(filename, 'r') as f:
return f.read()
def extract_secrets(content: str) -> List[str]:
"""Extract all secrets marked between {* and *} from content."""
return re.findall(r'\{\* (.*?) \*\}', content)
def categorize_secrets(secrets: List[str]) -> Dict[str, int]:
"""Count secrets by their category prefix (text before the first colon)."""
categories: Dict[str, int] = {}
for secret in secrets:
key = secret.split(":")[0].strip() if ":" in secret else "UNKNOWN"
categories[key] = categories.get(key, 0) + 1
return categories
def decode_blueprint_safe(filename: str) -> List[str]:
"""Full pipeline: read file safely and extract secrets."""
try:
content = read_blueprint(filename)
return extract_secrets(content)
except FileNotFoundError:
print(f"Error: '{filename}' not found.")
return []
Why this is better: Each function has a single responsibility, making it independently testable. The decode_blueprint_safe orchestrator is now easy to read and modify.
Refactoring prompt to give Copilot:
// Refactor the BlueprintDecoder class by extracting:
// 1. ReadBlueprint(string filename) -> string - file I/O only
// 2. ExtractSecrets(string content) -> List<string> - regex parsing only
// 3. CategorizeSecrets(List<string> secrets) -> Dictionary<string, int> - categorization only
After (refactored):
using System.IO;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using System.Linq;
public class BlueprintDecoder
{
private static readonly Regex SecretPattern = new Regex(@"\{\* (.*?) \*\}", RegexOptions.Compiled);
/// <summary>Read and return the raw content of a blueprint file.</summary>
public static string ReadBlueprint(string filename) => File.ReadAllText(filename);
/// <summary>Extract all secrets marked between {* and *} from content.</summary>
public static List<string> ExtractSecrets(string content) =>
SecretPattern.Matches(content)
.Select(m => m.Groups[1].Value)
.ToList();
/// <summary>Count secrets by their category prefix (text before the first colon).</summary>
public static Dictionary<string, int> CategorizeSecrets(List<string> secrets) =>
secrets.GroupBy(s => s.Contains(':') ? s.Split(':')[0].Trim() : "UNKNOWN")
.ToDictionary(g => g.Key, g => g.Count());
/// <summary>Full pipeline: read file safely and extract secrets.</summary>
public static List<string> DecodeBlueprintSafe(string filename)
{
try { return ExtractSecrets(ReadBlueprint(filename)); }
catch (FileNotFoundException) { Console.WriteLine($"Error: '{filename}' not found."); return new(); }
}
}
Refactoring prompt to give Copilot:
/**
* Refactor the decode function into three helpers:
* 1. readBlueprint(filename): string - file I/O only
* 2. extractSecrets(content): string[] - regex parsing only
* 3. categorizeSecrets(secrets): Record<string, number> - counts by prefix before ":"
*/
After (refactored):
const fs = require('fs');
/** Read and return the raw content of a blueprint file. */
function readBlueprint(filename) {
return fs.readFileSync(filename, 'utf8');
}
/** Extract all secrets marked between {* and *} from content. */
function extractSecrets(content) {
return [...content.matchAll(/\{\* (.*?) \*\}/g)].map(m => m[1]);
}
/** Count secrets by their category prefix (text before the first colon). */
function categorizeSecrets(secrets) {
return secrets.reduce((acc, s) => {
const key = s.includes(':') ? s.split(':')[0].trim() : 'UNKNOWN';
acc[key] = (acc[key] ?? 0) + 1;
return acc;
}, {});
}
/** Full pipeline: read file safely and extract secrets. */
function decodeBlueprintSafe(filename) {
try {
return extractSecrets(readBlueprint(filename));
} catch (err) {
console.log(`Error: '${filename}' not found.`);
return [];
}
}
Part 3, Task 2: Writing Unit Tests with Copilot
Prompt Copilot to generate a Playwright test suite for each helper function. The ideal suite covers happy paths, edge cases, and error conditions.
Prompt to give Copilot:
Using pytest-playwright, write tests for extract_secrets() and categorize_secrets() in blueprint_decoder.py.
Cover: happy path, no matches, multiple secrets, categorization by prefix, and no-colon edge case.
Install with: pip install pytest-playwright
What Copilot should generate:
import pytest
from blueprint_decoder import extract_secrets, categorize_secrets
def test_extract_secrets_finds_all():
content = "data {* SECRET_ONE *} more {* KEY: VALUE *} end"
assert extract_secrets(content) == ["SECRET_ONE", "KEY: VALUE"]
def test_extract_secrets_no_matches():
assert extract_secrets("no secrets here") == []
def test_extract_secrets_empty_string():
assert extract_secrets("") == []
def test_categorize_secrets_by_prefix():
secrets = ["AGENT_CODENAME: SHADOWMIND", "VAULT_ACCESS_CODE: DELTA-7", "AGENT_CODENAME: GHOSTFIRE"]
result = categorize_secrets(secrets)
assert result == {"AGENT_CODENAME": 2, "VAULT_ACCESS_CODE": 1}
def test_categorize_secrets_no_colon():
assert categorize_secrets(["SECURE_COMMS_PROTOCOL"]) == {"UNKNOWN": 1}
def test_categorize_secrets_empty():
assert categorize_secrets([]) == {}
Run with: python -m pytest test_blueprint_decoder.py -v
Prompt to give Copilot:
Using Microsoft.Playwright.NUnit, write tests for BlueprintDecoder.ExtractSecrets and CategorizeSecrets.
Cover: happy path, no matches, empty input, and categorization edge cases (no colon).
Install with: dotnet add package Microsoft.Playwright.NUnit
What Copilot should generate:
using Microsoft.Playwright.NUnit;
using NUnit.Framework;
using System.Collections.Generic;
[TestFixture]
public class BlueprintDecoderTests : PlaywrightTest
{
[Test]
public void ExtractSecrets_FindsAllSecrets()
{
var content = "data {* SECRET_ONE *} more {* KEY: VALUE *} end";
var result = BlueprintDecoder.ExtractSecrets(content);
Assert.That(result, Is.EqualTo(new List<string> { "SECRET_ONE", "KEY: VALUE" }));
}
[Test]
public void ExtractSecrets_NoMatches_ReturnsEmpty()
{
Assert.That(BlueprintDecoder.ExtractSecrets("no secrets here"), Is.Empty);
}
[Test]
public void CategorizeSecrets_GroupsByPrefix()
{
var secrets = new List<string> { "AGENT_CODENAME: SHADOWMIND", "VAULT_ACCESS_CODE: DELTA-7", "AGENT_CODENAME: GHOSTFIRE" };
var result = BlueprintDecoder.CategorizeSecrets(secrets);
Assert.That(result["AGENT_CODENAME"], Is.EqualTo(2));
Assert.That(result["VAULT_ACCESS_CODE"], Is.EqualTo(1));
}
[Test]
public void CategorizeSecrets_NoColon_MarkedUnknown()
{
var result = BlueprintDecoder.CategorizeSecrets(new List<string> { "SECURE_COMMS_PROTOCOL" });
Assert.That(result["UNKNOWN"], Is.EqualTo(1));
}
}
Run with: dotnet test
Prompt to give Copilot:
Using @playwright/test, write tests for extractSecrets() and categorizeSecrets() in leagueMission.js.
Cover: happy path, no matches, empty string, secrets with and without a colon prefix.
Install with: npm install -D @playwright/test
What Copilot should generate:
import { test, expect } from '@playwright/test';
import { extractSecrets, categorizeSecrets } from './leagueMission';
test.describe('extractSecrets', () => {
test('finds all secrets', () => {
const content = 'data {* SECRET_ONE *} more {* KEY: VALUE *} end';
expect(extractSecrets(content)).toEqual(['SECRET_ONE', 'KEY: VALUE']);
});
test('returns empty array when no matches', () => {
expect(extractSecrets('no secrets here')).toEqual([]);
});
test('handles empty string', () => {
expect(extractSecrets('')).toEqual([]);
});
});
test.describe('categorizeSecrets', () => {
test('groups secrets by prefix', () => {
const secrets = ['AGENT_CODENAME: SHADOWMIND', 'VAULT_ACCESS_CODE: DELTA-7', 'AGENT_CODENAME: GHOSTFIRE'];
expect(categorizeSecrets(secrets)).toEqual({ AGENT_CODENAME: 2, VAULT_ACCESS_CODE: 1 });
});
test('marks secrets without colon as UNKNOWN', () => {
expect(categorizeSecrets(['SECURE_COMMS_PROTOCOL'])).toEqual({ UNKNOWN: 1 });
});
test('returns empty object for empty array', () => {
expect(categorizeSecrets([])).toEqual({});
});
});
Run with: npx playwright test blueprint_decoder.spec.ts
Part 3, Task 3: Documentation Generation
Prompt Copilot to write comprehensive docstrings. The key is giving it a comment that specifies what to document: purpose, parameters, return value, exceptions, and an example.
Prompt: Place your cursor inside a function and type:
# Write a detailed docstring covering: purpose, Args, Returns, Raises, and an Example
What Copilot should generate for extract_secrets:
def extract_secrets(content: str) -> List[str]:
"""
Extract all secrets marked between {* and *} delimiters from a string.
Searches the given content for all occurrences of the pattern {* ... *}
and returns the text found between each pair of markers.
Args:
content: The raw text to search. May contain zero or more secret markers.
Returns:
A list of extracted secret strings, in the order they appear.
Returns an empty list if no markers are found.
Example:
>>> extract_secrets("data {* VAULT_CODE: DELTA-7 *} and {* PROTOCOL *}")
['VAULT_CODE: DELTA-7', 'PROTOCOL']
"""
return re.findall(r'\{\* (.*?) \*\}', content)
What Copilot should generate for ExtractSecrets:
/// <summary>
/// Extracts all secrets marked between {* and *} delimiters from a string.
/// Searches the given content for all occurrences of the pattern {* ... *}
/// and returns the captured groups.
/// </summary>
/// <param name="content">
/// The raw text to search. May contain zero or more secret markers.
/// </param>
/// <returns>
/// A list of extracted secret strings in the order they appear.
/// Returns an empty list if no markers are found.
/// </returns>
/// <example>
/// <code>
/// var secrets = ExtractSecrets("data {* VAULT_CODE: DELTA-7 *} end");
/// // secrets == ["VAULT_CODE: DELTA-7"]
/// </code>
/// </example>
public static List<string> ExtractSecrets(string content) => ...
What Copilot should generate for extractSecrets:
/**
* Extract all secrets marked between {* and *} delimiters from a string.
*
* Searches the given content for all occurrences of the pattern {* ... *}
* and returns the text found between each pair of markers.
*
* @param {string} content - The raw text to search. May contain zero or more secret markers.
* @returns {string[]} An array of extracted secret strings in the order they appear.
* Returns an empty array if no markers are found.
*
* @example
* extractSecrets("data {* VAULT_CODE: DELTA-7 *} and {* PROTOCOL *}");
* // => ['VAULT_CODE: DELTA-7', 'PROTOCOL']
*/
function extractSecrets(content) { ... }
Part 4: Copilot Chat vs Agent Mode
These tasks are about recognising when to use Chat (Ask) Mode for targeted, surgical changes versus Agent (Autonomous) Mode for larger, multi-file tasks. The examples below show what good output looks like for each scenario.
Part 4, Task 1: Chat Mode - Targeted Improvement
The task is to add retry logic with exponential backoff to the network call. This is a focused, single-function change - ideal for Chat Mode.
Chat prompt to use:
Add retry logic with exponential backoff to the retrieve_and_decode_blueprint function.
Retry up to 3 times on network errors (ConnectionError, Timeout). Wait 1s, 2s, 4s between attempts.
Log each retry attempt. Do not change the function signature.
What Copilot should produce:
import requests
import re
import time
import logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
MAX_RETRIES = 3
BASE_DELAY = 1 # seconds
def retrieve_and_decode_blueprint(blueprint_url: str) -> None:
"""Retrieve a blueprint URL and print extracted secrets. Retries up to 3 times."""
last_error = None
for attempt in range(1, MAX_RETRIES + 1):
try:
response = requests.get(blueprint_url, timeout=10)
response.raise_for_status()
secrets = re.findall(r"\{\*(.*?)\*\}", response.text)
for idx, secret in enumerate(secrets, 1):
print(f"Secret #{idx}: {secret.strip()}")
return
except (requests.ConnectionError, requests.Timeout) as e:
last_error = e
wait = BASE_DELAY * (2 ** (attempt - 1))
logging.warning(f"Attempt {attempt}/{MAX_RETRIES} failed: {e}. Retrying in {wait}s...")
time.sleep(wait)
logging.error(f"All {MAX_RETRIES} attempts failed. Last error: {last_error}")
What Copilot should produce:
using System;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public class LeagueHQ
{
private static readonly HttpClient _client = new() { Timeout = TimeSpan.FromSeconds(10) };
private static readonly Regex _pattern = new(@"\{\*(.*?)\*\}", RegexOptions.Compiled);
private const int MaxRetries = 3;
private static async Task RetrieveAndDecodeBlueprint(string url)
{
Exception? lastError = null;
for (int attempt = 1; attempt <= MaxRetries; attempt++)
{
try
{
var content = await _client.GetStringAsync(url);
var matches = _pattern.Matches(content);
for (int i = 0; i < matches.Count; i++)
Console.WriteLine($"Secret #{i + 1}: {matches[i].Groups[1].Value.Trim()}");
return;
}
catch (HttpRequestException e)
{
lastError = e;
int wait = (int)Math.Pow(2, attempt - 1);
Console.Error.WriteLine($"Attempt {attempt}/{MaxRetries} failed: {e.Message}. Retrying in {wait}s...");
await Task.Delay(wait * 1000);
}
}
Console.Error.WriteLine($"All {MaxRetries} attempts failed. Last error: {lastError?.Message}");
}
}
What Copilot should produce:
const MAX_RETRIES = 3;
const BASE_DELAY_MS = 1000;
async function retrieveAndDecodeBlueprint(url) {
let lastError;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 10000);
const response = await fetch(url, { signal: controller.signal });
clearTimeout(timer);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const text = await response.text();
[...text.matchAll(/\{\*(.*?)\*\}/g)].forEach((m, i) =>
console.log(`Secret #${i + 1}: ${m[1].trim()}`)
);
return;
} catch (err) {
lastError = err;
const wait = BASE_DELAY_MS * Math.pow(2, attempt - 1);
console.warn(`Attempt ${attempt}/${MAX_RETRIES} failed: ${err.message}. Retrying in ${wait}ms...`);
await new Promise(r => setTimeout(r, wait));
}
}
console.error(`All ${MAX_RETRIES} attempts failed. Last error: ${lastError?.message}`);
}
Part 4, Task 2: Agent Mode - Scaffold a Full CLI Tool
This task spans multiple files and requires coordinating code, tests, and documentation - ideal for Agent Mode. Use the prompt for your chosen language below.
Agent Mode prompt: "Scaffold a Python CLI tool called league-decoder that: (1) accepts --source <url>, --marker <start:end>, and --max-secrets <n> arguments using argparse; (2) fetches the URL with a 10s timeout and 3 retries using requests; (3) extracts secrets between the markers using regex; (4) prints a formatted report to stdout; (5) includes a test_decoder.py with at least a happy-path and error-path test using pytest; (6) creates a README.md with usage examples."
What Agent Mode should produce (key files):
league-decoder/
├── cli.py ← entry point, argparse argument parsing
├── decoder.py ← fetch + extract logic with retries
├── formatter.py ← report formatting helper
├── test_decoder.py ← pytest tests: happy path + timeout simulation
└── README.md ← usage, flags, examples
Agent Mode output varies significantly — this is expected. Copilot's output is considered compliant if it meets the core requirements, regardless of the exact structure. Common variations you may see:
- Single-module layout — all logic in one file (e.g.,
blueprint_decoder2.py) rather than split acrosscli.py,decoder.py,formatter.py unittestinstead ofpytest— both are valid; Copilot may default to the stdlib test frameworkpyproject.toml/setup.py— Copilot may scaffold a full package structure with install metadata- Different flag names — e.g.,
--source-url,--marker-start,--marker-endinstead of--source,--marker
Check your output against these core requirements: CLI args ✓, retry/error handling ✓, structured logging ✓, unit tests (happy path + error path) ✓, README with usage ✓. If all five are present, it's a pass.
Expected cli.py outline:
import argparse
from decoder import fetch_and_decode
def main():
parser = argparse.ArgumentParser(description="League Blueprint Decoder")
parser.add_argument("--source", required=True, help="Blueprint URL to decode")
parser.add_argument("--marker", default="{*:*}", help="Secret delimiters (start:end)")
parser.add_argument("--max-secrets", type=int, default=10, help="Maximum secrets to display")
args = parser.parse_args()
start, end = args.marker.split(":")
fetch_and_decode(args.source, start, end, args.max_secrets)
if __name__ == "__main__":
main()
Agent Mode prompt: "Scaffold a .NET console CLI tool called LeagueDecoder that: (1) accepts --source <url>, --marker <start:end>, and --max-secrets <n> arguments; (2) fetches the URL with a 10s timeout and 3 retries using HttpClient; (3) extracts secrets between the markers using regex; (4) prints a formatted report to stdout; (5) includes an xUnit test project with at least a happy-path and error-path test; (6) creates a README.md with usage examples."
What Agent Mode should produce (key files):
LeagueDecoder/
├── Program.cs ← entry point, argument parsing
├── Decoder.cs ← fetch + extract logic with retries
├── Formatter.cs ← report formatting helper
├── LeagueDecoder.Tests/
│ └── DecoderTests.cs ← xUnit tests: happy path + timeout simulation
└── README.md ← usage, flags, examples
Expected Program.cs outline:
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
string source = GetArg(args, "--source") ?? throw new ArgumentException("--source is required");
string marker = GetArg(args, "--marker") ?? "{*:*}";
int maxSecrets = int.TryParse(GetArg(args, "--max-secrets"), out var n) ? n : 10;
var parts = marker.Split(':');
await Decoder.FetchAndDecodeAsync(source, parts[0], parts[1], maxSecrets);
}
static string? GetArg(string[] args, string flag)
{
int i = Array.IndexOf(args, flag);
return i >= 0 && i + 1 < args.Length ? args[i + 1] : null;
}
}
Agent Mode prompt: "Scaffold a Node.js CLI tool called league-decoder that: (1) accepts --source <url>, --marker <start:end>, and --max-secrets <n> flags; (2) fetches the URL with a 10s timeout and 3 retries; (3) extracts secrets between the markers; (4) prints a formatted report to stdout; (5) includes a decoder.test.js with at least a happy-path and error-path test; (6) creates a README.md with usage examples."
What Agent Mode should produce (key files):
league-decoder/
├── cli.js ← entry point, argument parsing
├── decoder.js ← fetch + extract logic with retries
├── formatter.js ← report formatting helper
├── decoder.test.js ← Jest tests: happy path + timeout simulation
└── README.md ← usage, flags, examples
Expected cli.js outline:
#!/usr/bin/env node
const { program } = require('commander');
const { fetchAndDecode } = require('./decoder');
program
.requiredOption('--source <url>', 'Blueprint URL to decode')
.option('--marker <start:end>', 'Secret delimiters', '{*:*}')
.option('--max-secrets <n>', 'Maximum secrets to display', '10')
.parse();
const opts = program.opts();
const [start, end] = opts.marker.split(':');
fetchAndDecode(opts.source, start, end, parseInt(opts.maxSecrets))
.catch(err => { console.error('Fatal:', err.message); process.exit(1); });
Key takeaway: Agent Mode is powerful for scaffolding, but always review the generated file structure and test output before trusting it fully. It may miss edge cases or make assumptions about your environment. Chat Mode is better when you only need one precise change.
Part 5: Security & Guardrails
Part 5 tasks are about hardening the decoder against real-world threats. Below are the ideal solutions for each guardrail category.
Part 5, Task 1: URL Validation (Allowlist)
Never trust user-supplied URLs. Add an allowlist check before making any network request.
from urllib.parse import urlparse
ALLOWED_HOSTS = {
"raw.githubusercontent.com",
"league-blueprints.internal",
}
def validate_url(url: str) -> None:
"""Raise ValueError if the URL's host is not in the allowlist."""
parsed = urlparse(url)
if parsed.scheme not in ("https",):
raise ValueError(f"URL must use HTTPS. Got: {parsed.scheme!r}")
if parsed.netloc not in ALLOWED_HOSTS:
raise ValueError(f"Host '{parsed.netloc}' is not in the allowed list.")
# Usage:
try:
validate_url("https://raw.githubusercontent.com/microsoft/CopilotAdventures/main/Data/scrolls.txt")
print("URL is valid")
except ValueError as e:
print(f"Blocked: {e}")
Expected Output (valid URL): URL is valid
Expected Output (blocked): Blocked: Host 'evil.example.com' is not in the allowed list.
using System;
public static class UrlValidator
{
private static readonly HashSet<string> AllowedHosts = new()
{
"raw.githubusercontent.com",
"league-blueprints.internal"
};
/// <summary>Throw ArgumentException if URL is not HTTPS or host is not allowed.</summary>
public static void ValidateUrl(string url)
{
var uri = new Uri(url);
if (uri.Scheme != "https")
throw new ArgumentException($"URL must use HTTPS. Got: '{uri.Scheme}'");
if (!AllowedHosts.Contains(uri.Host))
throw new ArgumentException($"Host '{uri.Host}' is not in the allowed list.");
}
}
const ALLOWED_HOSTS = new Set([
'raw.githubusercontent.com',
'league-blueprints.internal',
]);
/**
* Throw an Error if the URL is not HTTPS or the host is not in the allowlist.
* @param {string} url
*/
function validateUrl(url) {
const parsed = new URL(url);
if (parsed.protocol !== 'https:')
throw new Error(`URL must use HTTPS. Got: '${parsed.protocol}'`);
if (!ALLOWED_HOSTS.has(parsed.hostname))
throw new Error(`Host '${parsed.hostname}' is not in the allowed list.`);
}
Part 5, Task 2: Structured Logging (No Secret Leakage)
Audit logs should record that secrets were found - not what they are. Here's the pattern:
import logging
import json
from datetime import datetime, timezone
def log_mission_result(url: str, secrets_count: int, success: bool, reason: str = "") -> None:
"""Emit a structured audit log entry. Never log secret content."""
entry = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"source": url,
"secrets_recovered": secrets_count,
"success": success,
"reason": reason,
}
logging.info(json.dumps(entry))
# Good - logs metadata only:
log_mission_result("https://raw.githubusercontent.com/...", secrets_count=5, success=True)
# BAD - never do this:
# logging.info(f"Secrets: {secrets}") # exposes classified data
/**
* Emit a structured audit log. Never include actual secret values.
* @param {string} url
* @param {number} secretsCount
* @param {boolean} success
* @param {string} [reason]
*/
function logMissionResult(url, secretsCount, success, reason = '') {
console.log(JSON.stringify({
timestamp: new Date().toISOString(),
source: url,
secrets_recovered: secretsCount,
success,
reason,
}));
}
// Good - logs metadata only:
logMissionResult('https://raw.githubusercontent.com/...', 5, true);
// BAD - never do this:
// console.log('Secrets:', secrets); // exposes classified data
Part 5, Task 3: Security Test Cases
Every guardrail needs a corresponding test. Here's what the happy-path and failure-path tests should look like:
import pytest
from decoder import validate_url
def test_valid_https_url_passes():
validate_url("https://raw.githubusercontent.com/microsoft/CopilotAdventures/main/Data/scrolls.txt")
def test_http_url_is_blocked():
with pytest.raises(ValueError, match="must use HTTPS"):
validate_url("http://raw.githubusercontent.com/some/path")
def test_unknown_host_is_blocked():
with pytest.raises(ValueError, match="not in the allowed list"):
validate_url("https://evil.example.com/payload")
const { validateUrl } = require('./decoder');
test('valid HTTPS URL passes', () => {
expect(() => validateUrl('https://raw.githubusercontent.com/microsoft/CopilotAdventures/main/Data/scrolls.txt'))
.not.toThrow();
});
test('HTTP URL is blocked', () => {
expect(() => validateUrl('http://raw.githubusercontent.com/some/path'))
.toThrow('must use HTTPS');
});
test('unknown host is blocked', () => {
expect(() => validateUrl('https://evil.example.com/payload'))
.toThrow('not in the allowed list');
});
Part 6: Production Workflows
Part 6 is about assembling everything into a production-ready CLI tool with structured output, observability, and tests. The ideal deliverable is a working tool in one language; the examples below show what the key pieces should look like.
Part 6: The Production-Ready Decoder CLI
By the end of this lab your project should have this structure:
league-mission-control/
├── decoder.py ← core: validate, fetch (with retries), extract, cap, log
├── cli.py ← argument parsing + entry point
├── test_decoder.py ← pytest: happy path + failure path + empty payload
└── README.md ← quickstart, flags, example output
decoder.py - hardened core:
import re
import time
import json
import logging
import requests
from datetime import datetime, timezone
from urllib.parse import urlparse
from typing import List
logging.basicConfig(level=logging.INFO, format='%(message)s')
ALLOWED_HOSTS = {"raw.githubusercontent.com"}
SECRET_PATTERN = re.compile(r"\{\*(.*?)\*\}")
def validate_url(url: str) -> None:
parsed = urlparse(url)
if parsed.scheme != "https":
raise ValueError(f"URL must use HTTPS. Got: {parsed.scheme!r}")
if parsed.netloc not in ALLOWED_HOSTS:
raise ValueError(f"Host '{parsed.netloc}' is not in the allowed list.")
def fetch_with_retry(url: str, max_retries: int = 3, timeout: int = 10) -> str:
for attempt in range(1, max_retries + 1):
try:
r = requests.get(url, timeout=timeout)
r.raise_for_status()
return r.text
except (requests.ConnectionError, requests.Timeout, requests.HTTPError) as e:
if attempt == max_retries:
raise
wait = 2 ** (attempt - 1)
logging.warning(json.dumps({"event": "retry", "attempt": attempt, "wait_s": wait, "reason": str(e)}))
time.sleep(wait)
def extract_secrets(content: str, max_secrets: int = 10) -> List[str]:
return [m.strip()[:200] for m in SECRET_PATTERN.findall(content)][:max_secrets]
def run_mission(url: str, max_secrets: int = 10) -> None:
start = time.monotonic()
validate_url(url)
content = fetch_with_retry(url)
secrets = extract_secrets(content, max_secrets)
duration_ms = int((time.monotonic() - start) * 1000)
logging.info(json.dumps({
"timestamp": datetime.now(timezone.utc).isoformat(),
"source": url, "secrets_recovered": len(secrets),
"fetch_duration_ms": duration_ms, "success": True
}))
print(f"=== MISSION REPORT ({len(secrets)} secret(s)) ===")
for i, s in enumerate(secrets, 1):
print(f" {i}. {s}")
cli.py - entry point:
import argparse
from decoder import run_mission
def main():
parser = argparse.ArgumentParser(description="League Mission Control Decoder")
parser.add_argument("--source", required=True, help="Blueprint URL to decode")
parser.add_argument("--max-secrets", type=int, default=10, help="Maximum secrets to display")
args = parser.parse_args()
run_mission(args.source, args.max_secrets)
if __name__ == "__main__":
main()
test_decoder.py - test suite:
import pytest
from unittest.mock import patch, MagicMock
from decoder import validate_url, extract_secrets, run_mission
def test_validate_url_passes_allowed_host():
validate_url("https://raw.githubusercontent.com/microsoft/CopilotAdventures/main/Data/scrolls.txt")
def test_validate_url_blocks_http():
with pytest.raises(ValueError, match="must use HTTPS"):
validate_url("http://raw.githubusercontent.com/path")
def test_extract_secrets_caps_at_max():
content = " ".join(f"{{* SECRET_{i} *}}" for i in range(20))
assert len(extract_secrets(content, max_secrets=5)) == 5
def test_run_mission_happy_path(capsys):
with patch("decoder.fetch_with_retry", return_value="{* AGENT_CODENAME: SHADOWMIND *}"):
run_mission("https://raw.githubusercontent.com/org/repo/file.txt")
captured = capsys.readouterr()
assert "AGENT_CODENAME: SHADOWMIND" in captured.out
def test_run_mission_empty_response(capsys):
with patch("decoder.fetch_with_retry", return_value="no secrets here"):
run_mission("https://raw.githubusercontent.com/org/repo/file.txt")
captured = capsys.readouterr()
assert "0 secret(s)" in captured.out
Run with:
python cli.py --source https://raw.githubusercontent.com/microsoft/CopilotAdventures/main/Data/scrolls.txt
python -m pytest test_decoder.py -v
Project layout:
LeagueMissionControl/
├── Decoder.cs ← validate + fetch with retry + extract
├── Program.cs ← CLI entry point (System.CommandLine or args[])
├── DecoderTests.cs ← xUnit: happy path + timeout + empty response
└── README.md
Decoder.cs - key methods:
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public static class Decoder
{
private static readonly HttpClient Client = new() { Timeout = TimeSpan.FromSeconds(10) };
private static readonly Regex Pattern = new(@"\{\*(.*?)\*\}", RegexOptions.Compiled);
private static readonly HashSet<string> AllowedHosts = new() { "raw.githubusercontent.com" };
public static void ValidateUrl(string url)
{
var uri = new Uri(url);
if (uri.Scheme != "https") throw new ArgumentException($"Must use HTTPS. Got '{uri.Scheme}'");
if (!AllowedHosts.Contains(uri.Host)) throw new ArgumentException($"Host '{uri.Host}' not allowed.");
}
public static async Task<string> FetchWithRetry(string url, int maxRetries = 3)
{
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try { return await Client.GetStringAsync(url); }
catch (HttpRequestException e) when (attempt < maxRetries)
{
int wait = (int)Math.Pow(2, attempt - 1);
Console.Error.WriteLine($"Attempt {attempt} failed: {e.Message}. Retrying in {wait}s...");
await Task.Delay(wait * 1000);
}
}
return await Client.GetStringAsync(url); // final attempt - let it throw
}
public static List<string> ExtractSecrets(string content, int maxSecrets = 10)
{
var results = new List<string>();
foreach (Match m in Pattern.Matches(content))
{
if (results.Count >= maxSecrets) break;
var val = m.Groups[1].Value.Trim();
results.Add(val.Length > 200 ? val[..200] : val);
}
return results;
}
}
Project layout:
league-mission-control/
├── decoder.js ← validate + fetch with retry + extract
├── cli.js ← entry point with commander
├── decoder.test.js ← Jest tests
└── README.md
decoder.js - core:
const ALLOWED_HOSTS = new Set(['raw.githubusercontent.com']);
const SECRET_RE = /\{\*(.*?)\*\}/g;
function validateUrl(url) {
const p = new URL(url);
if (p.protocol !== 'https:') throw new Error(`Must use HTTPS. Got: '${p.protocol}'`);
if (!ALLOWED_HOSTS.has(p.hostname)) throw new Error(`Host '${p.hostname}' not allowed.`);
}
async function fetchWithRetry(url, maxRetries = 3) {
let lastErr;
for (let i = 1; i <= maxRetries; i++) {
try {
const ctrl = new AbortController();
const t = setTimeout(() => ctrl.abort(), 10000);
const res = await fetch(url, { signal: ctrl.signal });
clearTimeout(t);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
return await res.text();
} catch (e) {
lastErr = e;
if (i < maxRetries) {
const wait = Math.pow(2, i - 1) * 1000;
console.warn(`Attempt ${i} failed: ${e.message}. Retrying in ${wait}ms...`);
await new Promise(r => setTimeout(r, wait));
}
}
}
throw lastErr;
}
function extractSecrets(content, maxSecrets = 10) {
return [...content.matchAll(SECRET_RE)]
.map(m => m[1].trim().slice(0, 200))
.slice(0, maxSecrets);
}
module.exports = { validateUrl, fetchWithRetry, extractSecrets };
Run with:
node cli.js --source https://raw.githubusercontent.com/microsoft/CopilotAdventures/main/Data/scrolls.txt
npx jest decoder.test.js
Bonus 5: Copilot Coding Agent in Action
Bonus 5 is about delegating work to Copilot coding agent via GitHub Issues. The "solutions" are well-scoped issue descriptions. Use the templates below for each task and language.
Bonus 5, Task 1: Document the Decoder
Create an issue with a clear, documentation-only scope.
Issue title: Add comprehensive documentation to blueprint decoder
The blueprint decoder has been deployed to production, but lacks proper documentation.
New operatives cannot easily understand how to use it.
Requirements:
- Add docstrings to all functions in decoder.py and secret_extractor.py
- Follow PEP 257 conventions
- Include parameter types and descriptions
- Include return value documentation
- Add type hints where missing
- Do not modify function logic, only add documentation
- Ensure all docstrings are clear and helpful to new developers
Issue title: Add comprehensive documentation to blueprint decoder
The blueprint decoder has been deployed to production, but lacks proper documentation.
New operatives cannot easily understand how to use it.
Requirements:
- Add XML documentation comments to all public methods in Decoder.cs and SecretExtractor.cs
- Follow C# XML documentation conventions
- Include parameter descriptions and types
- Include return value documentation
- Add proper access modifiers and inheritance documentation
- Do not modify method logic, only add documentation
- Ensure all comments are clear and helpful to new developers
Issue title: Add comprehensive documentation to blueprint decoder
The blueprint decoder has been deployed to production, but lacks proper documentation.
New operatives cannot easily understand how to use it.
Requirements:
- Add JSDoc comments to all functions in decoder.js and secretExtractor.js
- Follow JSDoc conventions
- Include parameter types and descriptions
- Include return value documentation
- Add TypeScript-style type annotations in comments where applicable
- Do not modify function logic, only add documentation
- Ensure all comments are clear and helpful to new developers
Bonus 5, Task 2: Add Batch Processing Capabilities
Create an issue that scopes batch processing, retries, and tests.
Issue title: Implement batch blueprint processing with retry logic
The decoder currently processes one blueprint file at a time. The League now needs
the ability to process multiple files in a single batch, with proper error handling and retry logic.
Requirements:
- Create a new BatchProcessor class that accepts a list of blueprint file paths
- Implement exponential backoff retry logic (up to 3 retries with 1s, 2s, 4s delays)
- Failed files should be logged with error details
- Return a summary report including:
* Total files processed
* Successful extractions
* Failed files and reasons
* Total secrets extracted
- Write unit tests for all new methods
- Use async/await patterns for concurrent processing
- Ensure backwards compatibility with existing decoder functions
Issue title: Implement batch blueprint processing with retry logic
The decoder currently processes one blueprint file at a time. The League now needs
the ability to process multiple files in a single batch, with proper error handling and retry logic.
Requirements:
- Create a new BatchProcessor class that accepts a list of blueprint file paths
- Implement exponential backoff retry logic (up to 3 retries with 1s, 2s, 4s delays)
- Failed files should be logged with error details
- Return a summary report including:
* Total files processed
* Successful extractions
* Failed files and reasons
* Total secrets extracted
- Write unit tests for all new methods using xUnit
- Use Task and async/await for concurrent processing
- Ensure backwards compatibility with existing decoder methods
Issue title: Implement batch blueprint processing with retry logic
The decoder currently processes one blueprint file at a time. The League now needs
the ability to process multiple files in a single batch, with proper error handling and retry logic.
Requirements:
- Create a new BatchProcessor class that accepts a list of blueprint file paths
- Implement exponential backoff retry logic (up to 3 retries with 1s, 2s, 4s delays)
- Failed files should be logged with error details
- Return a summary report including:
* Total files processed
* Successful extractions
* Failed files and reasons
* Total secrets extracted
- Write unit tests for all new methods using Jest
- Use Promise and async/await for concurrent processing
- Ensure backwards compatibility with existing decoder functions
Bonus 5, Task 3: Generate Comprehensive Tests
Create an issue focused on unit test coverage and edge cases.
Issue title: Add comprehensive unit tests for secret extraction
The secret extraction module lacks proper test coverage.
We need unit tests that validate all edge cases and failure scenarios.
Requirements:
- Write tests for the extract_secrets() function covering:
* Normal extraction (secrets found between {* and *})
* Multiple secrets in a single file
* No secrets present (empty result)
* Malformed markers (incomplete {* or missing *})
* Empty or null input
* Files with special characters in secrets
- Write tests for the categorize_secret() function
- Achieve minimum 95% code coverage
- Use pytest framework with fixtures for test data
- Mock file I/O operations where appropriate
- Include parametrized tests for multiple input variations
- Run all tests as part of the validation
Issue title: Add comprehensive unit tests for secret extraction
The secret extraction module lacks proper test coverage.
We need unit tests that validate all edge cases and failure scenarios.
Requirements:
- Write tests for the ExtractSecrets() method covering:
* Normal extraction (secrets found between {* and *})
* Multiple secrets in a single file
* No secrets present (empty result)
* Malformed markers (incomplete {* or missing *})
* Empty or null input
* Files with special characters in secrets
- Write tests for the CategorizeSecret() method
- Achieve minimum 95% code coverage
- Use xUnit framework with shared fixtures for test data
- Mock file I/O operations where appropriate
- Use Theory attribute for parametrized tests with multiple input variations
- Run all tests as part of the validation
Issue title: Add comprehensive unit tests for secret extraction
The secret extraction module lacks proper test coverage.
We need unit tests that validate all edge cases and failure scenarios.
Requirements:
- Write tests for the extractSecrets() function covering:
* Normal extraction (secrets found between {* and *})
* Multiple secrets in a single file
* No secrets present (empty result)
* Malformed markers (incomplete {* or missing *})
* Empty or null input
* Files with special characters in secrets
- Write tests for the categorizeSecret() function
- Achieve minimum 95% code coverage
- Use Jest framework with test fixtures for test data
- Mock file I/O operations where appropriate
- Include parametrized tests for multiple input variations
- Run all tests as part of the validation
Part 7: Wrap-Up & Next Steps
Part 7 is reflective and personal - there are no single "correct" answers. The tasks below offer prompts and examples to help you consolidate what you've learned and plan your next steps with Copilot.
Part 7, Task 1: Discover Features You Haven't Used
Here are some high-value Copilot features that most developers haven't explored yet:
- Copilot Chat context commands: Type
#file,#selection, or#codebasein Chat to give Copilot targeted context without copy-pasting. @workspaceagent: Ask questions about your entire project - e.g., "What files handle error logging?" - and Copilot will search across all files.- Inline Chat (
Ctrl+I/Cmd+I): Open a chat prompt directly inside the editor to refactor a selected block without switching panels. - Fix & Explain in the Problems panel: Right-click any error in the Problems panel and select "Fix using Copilot" or "Explain using Copilot".
- Terminal Copilot (
Ctrl+Iin the terminal): Ask for shell commands in natural language - e.g., "find all Python files modified in the last 7 days". - Copilot Instructions (
.github/copilot-instructions.md): Add a file with project-specific conventions and Copilot will respect them in every suggestion.
Part 7, Task 2: Build Your Integration Plan
A well-structured integration plan answers these questions for your own work. Fill in each section:
# My Copilot Integration Plan
## 1. Where I'll use Copilot first
# (e.g., "Writing unit tests for the auth module - I always avoid it, Copilot can scaffold them fast")
## 2. Workflow rules I'll establish
# (e.g., "Never accept a Copilot suggestion without reading every line, especially security-sensitive code")
## 3. Prompting techniques I'll adopt from today
# (e.g., "Always add type hints + a docstring before writing function bodies")
## 4. One thing I'll try in Agent Mode this week
# (e.g., "Refactor the legacy CSV parser into smaller, tested functions")
## 5. My 'Copilot home base' bookmarks
# - GitHub Copilot docs: https://docs.github.com/en/copilot
# - What's new in Copilot: https://github.blog/changelog/label/copilot/
# - Responsible AI with Copilot: https://docs.github.com/en/copilot/responsible-use-of-github-copilot-features
Part 7, Task 3: Reflection Prompts
Use Copilot Chat to answer these reflection questions - it can help you articulate what you've learned:
- "Based on the code in this workspace, where would Copilot's suggestions be most and least reliable?"
- "What are the security risks of accepting Copilot suggestions without review in a production codebase?"
- "Explain the difference between using Copilot as an autocomplete tool vs. as a pair programmer."
- "What makes a good prompt for Copilot? Give me three principles and an example of each."
Key takeaway: Copilot is most powerful when you treat it as a junior engineer who needs clear, detailed instructions. The more context and intent you provide, the better its output. You remain responsible for every line of code that ships.
Troubleshooting Common Issues
Problem: Code won't run / Syntax errors
- Double-check indentation (Python is especially sensitive)
- Verify all brackets and parentheses are balanced
- Make sure you're using the right syntax for your language (e.g., `===` in JavaScript, not `==`)
- Compare your code to the solutions above character-by-character
Problem: Code runs but produces wrong output
- Check the expected output on this page
- Add `print()` or `console.log()` statements to debug intermediate values
- Verify your function logic matches the ideal solution
- Check for edge cases (negative numbers, zero, large numbers)
Problem: Error handling doesn't work
- Make sure the error is actually being raised/thrown in the code
- Verify your try/catch (or try/except) block is in the right place
- Check that the error message matches what's being caught
- Test with the exact input shown in the expected output (e.g., -5)
Problem: Copilot didn't generate the code I needed
- Improve your comment with more specific requirements
- Add type hints or function signatures
- Provide an example in your comment of what you want
- Try using Copilot Chat (Ctrl+I) instead of inline suggestions
Key Principles to Remember
- Test everything: Even if Copilot generates code, you're responsible for verifying it works
- Better comments = Better code: Detailed requirements produce better suggestions
- Iterate: If Copilot's first suggestion isn't perfect, refine your prompt and try again
- Understand what you're running: Never execute code you don't understand
- Use this reference for learning: Compare your approach with these solutions, but try to solve problems on your own first